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