auth0 example
This commit is contained in:
commit
6022dd7cf7
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.env
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "xatci"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Christine Dodrill <me@christine.website>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-rt = "1"
|
||||||
|
actix-service = "1"
|
||||||
|
actix-web = "2"
|
||||||
|
actix-web-httpauth = "0.4"
|
||||||
|
alcoholic_jwt = "1"
|
||||||
|
anyhow = "1.0"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
derive_more = "0"
|
||||||
|
diesel = { version = "1", features = ["postgres","uuidv07", "r2d2", "chrono"] }
|
||||||
|
futures = "0.3"
|
||||||
|
kankyo = "0.3"
|
||||||
|
log = "0.4"
|
||||||
|
pretty_env_logger = "0"
|
||||||
|
r2d2 = "0.8"
|
||||||
|
reqwest = { version = "0.10", features = ["json"] }
|
||||||
|
serde_json = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
thiserror = "1.0"
|
|
@ -0,0 +1,5 @@
|
||||||
|
# For documentation on how to configure this file,
|
||||||
|
# see diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
|
[print_schema]
|
||||||
|
file = "src/schema.rs"
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- This file was automatically created by Diesel to setup helper functions
|
||||||
|
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||||
|
-- changes will be added to existing projects as new migrations.
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||||
|
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- This file was automatically created by Diesel to setup helper functions
|
||||||
|
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||||
|
-- changes will be added to existing projects as new migrations.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Sets up a trigger for the given table to automatically set a column called
|
||||||
|
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||||
|
-- in the modified columns)
|
||||||
|
--
|
||||||
|
-- # Example
|
||||||
|
--
|
||||||
|
-- ```sql
|
||||||
|
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||||
|
--
|
||||||
|
-- SELECT diesel_manage_updated_at('users');
|
||||||
|
-- ```
|
||||||
|
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||||
|
BEGIN
|
||||||
|
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (
|
||||||
|
NEW IS DISTINCT FROM OLD AND
|
||||||
|
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||||
|
) THEN
|
||||||
|
NEW.updated_at := current_timestamp;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE users;
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
first_name TEXT NOT NULL,
|
||||||
|
last_name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
let pkgs = import <nixpkgs> { };
|
||||||
|
in pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
rustc
|
||||||
|
cargo
|
||||||
|
rls
|
||||||
|
rustfmt
|
||||||
|
openssl
|
||||||
|
pkg-config
|
||||||
|
postgresql
|
||||||
|
|
||||||
|
diesel-cli
|
||||||
|
];
|
||||||
|
|
||||||
|
DATABASE_URL = "postgres://xatci:hunter2@127.0.0.1/xatci?sslmode=disable";
|
||||||
|
DOMAIN = "https://xeserv.auth0.com/";
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::errors::ServiceError;
|
||||||
|
use alcoholic_jwt::{token_kid, validate, Validation, JWKS};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Claims {
|
||||||
|
sub: String,
|
||||||
|
company: String,
|
||||||
|
exp: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_token(token: &str) -> Result<bool, ServiceError> {
|
||||||
|
let domain = std::env::var("DOMAIN").expect("DOMAIN must be set");
|
||||||
|
let jwks = fetch_jwks(&format!("{}{}", domain.as_str(), ".well-known/jwks.json")).await
|
||||||
|
.expect("failed to fetch jwks");
|
||||||
|
let validations = vec![Validation::Issuer(domain), Validation::SubjectPresent];
|
||||||
|
let kid = match token_kid(&token) {
|
||||||
|
Ok(res) => res.expect("failed to decode kid"),
|
||||||
|
Err(_) => return Err(ServiceError::JWKSFetchError),
|
||||||
|
};
|
||||||
|
let jwk = jwks.find(&kid).expect("Specified key not found in set");
|
||||||
|
let res = validate(token, jwk, validations);
|
||||||
|
Ok(res.is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_jwks(uri: &str) -> anyhow::Result<JWKS> {
|
||||||
|
let res = reqwest::get(uri).await?;
|
||||||
|
let val = res.json::<JWKS>().await?;
|
||||||
|
return Ok(val);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
use actix_web::{error::ResponseError, HttpResponse};
|
||||||
|
use derive_more::Display;
|
||||||
|
|
||||||
|
#[derive(Debug, Display)]
|
||||||
|
pub enum ServiceError {
|
||||||
|
#[display(fmt = "Internal Server Error")]
|
||||||
|
InternalServerError,
|
||||||
|
|
||||||
|
#[display(fmt = "BadRequest: {}", _0)]
|
||||||
|
BadRequest(String),
|
||||||
|
|
||||||
|
#[display(fmt = "JWKSFetchError")]
|
||||||
|
JWKSFetchError,
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl ResponseError trait allows to convert our errors into http responses with appropriate data
|
||||||
|
impl ResponseError for ServiceError {
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
match self {
|
||||||
|
ServiceError::InternalServerError => {
|
||||||
|
HttpResponse::InternalServerError().json("Internal Server Error, Please try later")
|
||||||
|
}
|
||||||
|
ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
|
||||||
|
ServiceError::JWKSFetchError => {
|
||||||
|
HttpResponse::InternalServerError().json("Could not fetch JWKS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
use super::models::{NewUser, User};
|
||||||
|
use super::schema::users::dsl::*;
|
||||||
|
use super::Pool;
|
||||||
|
use crate::diesel::QueryDsl;
|
||||||
|
use crate::diesel::RunQueryDsl;
|
||||||
|
use actix_web::{web, Error, HttpResponse, Responder};
|
||||||
|
use diesel::dsl::{delete, insert_into};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::vec::Vec;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct InputUser {
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_users(db: web::Data<Pool>) -> Result<HttpResponse, Error> {
|
||||||
|
Ok(web::block(move || get_all_users(db))
|
||||||
|
.await
|
||||||
|
.map(|user| HttpResponse::Ok().json(user))
|
||||||
|
.map_err(|_| HttpResponse::InternalServerError())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_all_users(pool: web::Data<Pool>) -> Result<Vec<User>, diesel::result::Error> {
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
let items = users.load::<User>(&conn)?;
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for GET /users/{id}
|
||||||
|
pub async fn get_user_by_id(
|
||||||
|
db: web::Data<Pool>,
|
||||||
|
user_id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
Ok(
|
||||||
|
web::block(move || db_get_user_by_id(db, user_id.into_inner()))
|
||||||
|
.await
|
||||||
|
.map(|user| HttpResponse::Ok().json(user))
|
||||||
|
.map_err(|_| HttpResponse::InternalServerError())?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for POST /users
|
||||||
|
pub async fn add_user(
|
||||||
|
db: web::Data<Pool>,
|
||||||
|
item: web::Json<InputUser>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
Ok(web::block(move || add_single_user(db, item))
|
||||||
|
.await
|
||||||
|
.map(|user| HttpResponse::Created().json(user))
|
||||||
|
.map_err(|_| HttpResponse::InternalServerError())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for DELETE /users/{id}
|
||||||
|
pub async fn delete_user(
|
||||||
|
db: web::Data<Pool>,
|
||||||
|
user_id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
Ok(
|
||||||
|
web::block(move || delete_single_user(db, user_id.into_inner()))
|
||||||
|
.await
|
||||||
|
.map(|user| HttpResponse::Ok().json(user))
|
||||||
|
.map_err(|_| HttpResponse::InternalServerError())?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_get_user_by_id(pool: web::Data<Pool>, user_id: i32) -> Result<User, diesel::result::Error> {
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
users.find(user_id).get_result::<User>(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_single_user(
|
||||||
|
db: web::Data<Pool>,
|
||||||
|
item: web::Json<InputUser>,
|
||||||
|
) -> Result<User, diesel::result::Error> {
|
||||||
|
let conn = db.get().unwrap();
|
||||||
|
let new_user = NewUser {
|
||||||
|
first_name: &item.first_name,
|
||||||
|
last_name: &item.last_name,
|
||||||
|
email: &item.email,
|
||||||
|
created_at: chrono::Local::now().naive_local(),
|
||||||
|
};
|
||||||
|
let res = insert_into(users).values(&new_user).get_result(&conn)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_single_user(db: web::Data<Pool>, user_id: i32) -> Result<usize, diesel::result::Error> {
|
||||||
|
let conn = db.get().unwrap();
|
||||||
|
let count = delete(users.find(user_id)).execute(&conn)?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use actix_web::{dev::ServiceRequest, web, App, Error, HttpServer};
|
||||||
|
use actix_web_httpauth::{
|
||||||
|
extractors::{
|
||||||
|
bearer::{BearerAuth, Config},
|
||||||
|
AuthenticationError,
|
||||||
|
},
|
||||||
|
middleware::HttpAuthentication,
|
||||||
|
};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::r2d2::{self, ConnectionManager};
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
mod errors;
|
||||||
|
mod handlers;
|
||||||
|
mod models;
|
||||||
|
mod schema;
|
||||||
|
|
||||||
|
pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
|
#[actix_rt::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let _ = kankyo::init();
|
||||||
|
pretty_env_logger::init();
|
||||||
|
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
|
// create db connection pool
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||||
|
let pool: Pool = r2d2::Pool::builder()
|
||||||
|
.build(manager)
|
||||||
|
.expect("Failed to create pool.");
|
||||||
|
|
||||||
|
// Start http server
|
||||||
|
HttpServer::new(move || {
|
||||||
|
let auth = HttpAuthentication::bearer(validator);
|
||||||
|
App::new()
|
||||||
|
.wrap(auth)
|
||||||
|
.data(pool.clone())
|
||||||
|
.route("/users", web::get().to(handlers::get_users))
|
||||||
|
.route("/users/{id}", web::get().to(handlers::get_user_by_id))
|
||||||
|
.route("/users", web::post().to(handlers::add_user))
|
||||||
|
.route("/users/{id}", web::delete().to(handlers::delete_user))
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8080")?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error> {
|
||||||
|
let config = req
|
||||||
|
.app_data::<Config>()
|
||||||
|
.map(|data| data.get_ref().clone())
|
||||||
|
.unwrap_or_else(Default::default);
|
||||||
|
match auth::validate_token(credentials.token()).await {
|
||||||
|
Ok(res) => {
|
||||||
|
if res == true {
|
||||||
|
Ok(req)
|
||||||
|
} else {
|
||||||
|
Err(AuthenticationError::from(config).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(AuthenticationError::from(config).into()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
use crate::schema::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Queryable)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: i32,
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub created_at: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[table_name = "users"]
|
||||||
|
pub struct NewUser<'a> {
|
||||||
|
pub first_name: &'a str,
|
||||||
|
pub last_name: &'a str,
|
||||||
|
pub email: &'a str,
|
||||||
|
pub created_at: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
table! {
|
||||||
|
users (id) {
|
||||||
|
id -> Int4,
|
||||||
|
first_name -> Text,
|
||||||
|
last_name -> Text,
|
||||||
|
email -> Text,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue