start extending bespoke into another *forum*

This commit is contained in:
Viv Lim 2023-07-01 23:16:22 -07:00
parent b1d0cef3c9
commit c1fd43389b
12 changed files with 507 additions and 198 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
*/result
*/bespoke.toml
*/.env

124
Cargo.lock generated
View File

@ -96,7 +96,7 @@ checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -124,7 +124,7 @@ checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043"
dependencies = [
"async-trait",
"axum-core",
"bitflags",
"bitflags 1.3.2",
"bytes",
"futures-util",
"headers",
@ -180,6 +180,8 @@ dependencies = [
"axum",
"clap",
"config",
"diesel",
"dotenvy",
"headers",
"http",
"oauth2",
@ -207,6 +209,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "blake3"
version = "0.3.8"
@ -255,6 +263,12 @@ version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.2.1"
@ -321,7 +335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30607dd93c420c6f1f80b544be522a0238a7db35e6a12968d28910983fee0df0"
dependencies = [
"atty",
"bitflags",
"bitflags 1.3.2",
"clap_derive",
"clap_lex",
"once_cell",
@ -339,7 +353,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -436,6 +450,40 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
[[package]]
name = "diesel"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c"
dependencies = [
"bitflags 2.3.3",
"byteorder",
"diesel_derives",
"itoa",
"pq-sys",
]
[[package]]
name = "diesel_derives"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303"
dependencies = [
"diesel_table_macro_syntax",
"proc-macro2",
"quote",
"syn 2.0.22",
]
[[package]]
name = "diesel_table_macro_syntax"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
dependencies = [
"syn 2.0.22",
]
[[package]]
name = "digest"
version = "0.9.0"
@ -461,6 +509,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "encoding_rs"
version = "0.8.31"
@ -581,7 +635,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"ignore",
"walkdir",
]
@ -621,7 +675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [
"base64",
"bitflags",
"bitflags 1.3.2",
"bytes",
"headers-core",
"http",
@ -1073,7 +1127,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -1143,7 +1197,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -1164,6 +1218,15 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "pq-sys"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd"
dependencies = [
"vcpkg",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -1173,7 +1236,7 @@ dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
"version_check",
]
@ -1190,18 +1253,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.46"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
dependencies = [
"proc-macro2",
]
@ -1242,7 +1305,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
@ -1332,7 +1395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64",
"bitflags",
"bitflags 1.3.2",
"serde",
]
@ -1415,7 +1478,7 @@ checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -1572,6 +1635,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.1"
@ -1626,7 +1700,7 @@ checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -1681,7 +1755,7 @@ checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -1740,7 +1814,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"bytes",
"futures-core",
"futures-util",
@ -1786,7 +1860,7 @@ checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
]
[[package]]
@ -1950,6 +2024,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"
@ -2004,7 +2084,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
"wasm-bindgen-shared",
]
@ -2038,7 +2118,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.101",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View File

@ -23,4 +23,7 @@ async-trait = "0.1.57"
http = "0.2"
tera = "1.17"
anyhow = "*"
clap = { version = "4.0", features = [ "derive" ]}
clap = { version = "4.0", features = [ "derive" ]}
diesel = { version = "2.1.0", features = ["postgres"] }
dotenvy = "0.15"

9
server/diesel.toml Normal file
View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

0
server/migrations/.keep Normal file
View File

View File

@ -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();

View File

@ -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;

View File

@ -0,0 +1,11 @@
-- This file should undo anything in `up.sql`
-- Have to drop in reverse order because of dependencies
DROP TABLE reactions;
DROP TABLE posts;
DROP TABLE threads;
DROP TABLE boards;
DROP TABLE profiles;
DROP TABLE users;
DROP TABLE approvals;

View File

@ -0,0 +1,55 @@
-- Your SQL goes here
CREATE TABLE approvals ( -- represents something that an admin has approved. objects can be tracked back to their original approval, making them somewhat auditable
id SERIAL PRIMARY KEY,
description VARCHAR NOT NULL,
approved BOOLEAN NOT NULL
-- thinking approvals will need to have an owner user / profile, and allow for multiple events, and a timestamp
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
external_user_id VARCHAR NOT NULL,
active BOOLEAN NOT NULL,
approval_id INTEGER REFERENCES approvals(id)
);
CREATE TABLE profiles (
id SERIAL PRIMARY KEY,
display_name VARCHAR NOT NULL,
user_id INTEGER REFERENCES users(id)
-- nullable because profiles may be reflections of remote users, or system profiles controlled by nobody.
);
CREATE TABLE boards (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
description VARCHAR
);
CREATE TABLE threads (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
author_id INTEGER REFERENCES profiles(id),
-- nullable in case users are deleted
board_id INTEGER REFERENCES boards(id),
locked BOOLEAN NOT NULL,
sticky BOOLEAN NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
text VARCHAR NOT NULL,
author_id INTEGER REFERENCES profiles(id),
-- nullable in case users are deleted
thread_id INTEGER REFERENCES threads(id),
-- would content warnings be attached to posts?
reply_to INTEGER -- not a foreign key constraint because a circular reference might cause problems.
);
CREATE TABLE reactions (
id SERIAL PRIMARY KEY,
content VARCHAR NOT NULL,
author_id INTEGER REFERENCES profiles(id),
-- nullable in case users are deleted
post_id INTEGER REFERENCES posts(id)
);

196
server/src/auth/mod.rs Normal file
View File

@ -0,0 +1,196 @@
use config::Config;
use async_session::{MemoryStore, Session, SessionStore, async_trait};
use axum::{
routing::{get, post},
http::{StatusCode, HeaderMap, header::SET_COOKIE},
response::{IntoResponse, Redirect, Response},
Json, Router, Extension, extract::{Query, FromRequest, RequestParts},
extract::{
TypedHeader, rejection::TypedHeaderRejectionReason
}
};
use http::{header};
use oauth2::{
basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId,
ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl,
};
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, collections::HashSet, sync::Arc, path::Path};
use tokio::sync::Mutex;
use tera::{Tera, Context};
use clap::Parser;
use crate::{templating::TeraWrapper, Settings};
static COOKIE_NAME: &str = "SESSION";
#[derive(Clone)]
pub struct AuthState {
pub oauth_client: BasicClient,
/// store single-use csrf tokens globally
valid_csrf_states: Arc<Mutex<HashSet<String>>>
}
impl AuthState {
pub fn new(oauth_client: BasicClient) -> Self{
Self {
oauth_client,
valid_csrf_states: Arc::new(Mutex::new(HashSet::new()))
}
}
pub async fn store_csrf_token(&self, token: CsrfToken) -> bool {
self.valid_csrf_states.lock().await.insert(token.secret().clone())
}
pub async fn validate_csrf_token(&self, token: &str) -> bool {
self.valid_csrf_states.lock().await.remove(token)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
sub: String,
email_verified: bool,
name: String,
preferred_username: String,
email: String,
}
pub async fn oauth2_login(Extension(auth_state): Extension<AuthState>, Extension(settings): Extension<Settings>) -> Response {
let mut request_builder = auth_state.oauth_client.authorize_url(CsrfToken::new_random);
for scope in settings.scopes {
request_builder = request_builder.add_scope(oauth2::Scope::new(scope));
}
let (auth_url, csrf_token) = request_builder.url();
if !auth_state.store_csrf_token(csrf_token).await {
// Generated a CSRF token that already existed... this is bad
return (StatusCode::INTERNAL_SERVER_ERROR, "internal error: duplicate state, try again").into_response();
}
// Redirect to oauth2 provider
Redirect::to(&auth_url.to_string()).into_response()
}
// Valid user session required. If there is none, redirect to the auth page
pub async fn protected(user: User) -> impl IntoResponse {
format!(
"Welcome to the protected area :)\nHere's your info:\n{:?}",
user
)
}
pub async fn logout(
Extension(store): Extension<MemoryStore>,
TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> impl IntoResponse {
let cookie = cookies.get(COOKIE_NAME).unwrap();
let session = match store.load_session(cookie.to_string()).await.unwrap() {
Some(s) => s,
// No session active, just redirect
None => return Redirect::to("/"),
};
store.destroy_session(session).await.unwrap();
Redirect::to("/")
}
#[derive(Debug, Deserialize)]
pub struct AuthRequest {
code: String,
state: String,
}
pub async fn login_authorized(
Query(query): Query<AuthRequest>,
Extension(store): Extension<MemoryStore>,
Extension(auth_state): Extension<AuthState>,
Extension(settings): Extension<Settings>,
) -> Response {
if !auth_state.validate_csrf_token(&query.state).await {
// the auth request state isn't valid, reject this attempt.
return (StatusCode::FORBIDDEN, "auth request state isn't valid, try again").into_response();
}
// Get an auth token
let token = auth_state.oauth_client
.exchange_code(AuthorizationCode::new(query.code.clone()))
.request_async(async_http_client)
.await
.unwrap();
// Fetch user data from oauth provider
let client = reqwest::Client::new();
let user_data: User = client
.get(settings.userinfo_url)
.bearer_auth(token.access_token().secret())
.send()
.await
.unwrap()
.json::<User>()
.await
.unwrap();
// Create a new session filled with user data
let mut session = Session::new();
session.insert("user", &user_data).unwrap();
// Store session and get corresponding cookie
let cookie = store.store_session(session).await.unwrap().unwrap();
// Build the cookie
let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie);
// Set cookie
let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, cookie.parse().unwrap());
(headers, Redirect::to("/")).into_response()
}
pub struct AuthRedirect;
impl IntoResponse for AuthRedirect {
fn into_response(self) -> Response {
Redirect::temporary("/login").into_response()
}
}
#[async_trait]
impl<B> FromRequest<B> for User
where
B: Send,
{
// If anything goes wrong or no session is found, redirect to the auth page
type Rejection = AuthRedirect;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
let Extension(store) = Extension::<MemoryStore>::from_request(req)
.await
.expect("`MemoryStore` extension is missing");
let cookies = TypedHeader::<headers::Cookie>::from_request(req)
.await
.map_err(|e| match *e.name() {
header::COOKIE => match e.reason() {
TypedHeaderRejectionReason::Missing => AuthRedirect,
_ => panic!("unexpected error getting Cookie header(s): {}", e),
},
_ => panic!("unexpected error getting cookies: {}", e),
})?;
let session_cookie = cookies.get(COOKIE_NAME).ok_or(AuthRedirect)?;
let session = store
.load_session(session_cookie.to_string())
.await
.unwrap()
.ok_or(AuthRedirect)?;
let user = session.get::<User>("user").ok_or(AuthRedirect)?;
Ok(user)
}
}

View File

@ -1,3 +1,4 @@
use auth::User;
use config::Config;
use async_session::{MemoryStore, Session, SessionStore, async_trait};
use axum::{
@ -20,11 +21,11 @@ use tokio::sync::Mutex;
use tera::{Tera, Context};
use clap::Parser;
use crate::templating::TeraWrapper;
use crate::{templating::TeraWrapper, auth::{AuthState, oauth2_login, login_authorized, protected, logout}};
pub mod templating;
pub mod auth;
static COOKIE_NAME: &str = "SESSION";
#[derive(Parser, Debug)]
@ -36,7 +37,7 @@ struct Args {
}
#[derive(Debug, Deserialize, Clone)]
struct Settings {
pub struct Settings {
listen_addr: String,
client_id: String,
client_secret: String,
@ -56,33 +57,6 @@ impl Settings {
}
}
#[derive(Clone)]
struct AuthState {
pub oauth_client: BasicClient,
/// store single-use csrf tokens globally
valid_csrf_states: Arc<Mutex<HashSet<String>>>
}
impl AuthState {
pub fn new(oauth_client: BasicClient) -> Self{
Self {
oauth_client,
valid_csrf_states: Arc::new(Mutex::new(HashSet::new()))
}
}
pub async fn store_csrf_token(&self, token: CsrfToken) -> bool {
self.valid_csrf_states.lock().await.insert(token.secret().clone())
}
pub async fn validate_csrf_token(&self, token: &str) -> bool {
self.valid_csrf_states.lock().await.remove(token)
}
}
// axum oauth sample
// https://github.com/tokio-rs/axum/blob/0.5.x/examples/oauth/src/main.rs
@ -145,149 +119,4 @@ async fn root(Extension(tera): Extension<TeraWrapper>, user: Option<User>) -> im
}).unwrap();
context.try_insert("user", &user).unwrap();
tera.build_response("index.html", &context)
}
#[derive(Debug, Serialize, Deserialize)]
struct User {
sub: String,
email_verified: bool,
name: String,
preferred_username: String,
email: String,
}
async fn oauth2_login(Extension(auth_state): Extension<AuthState>, Extension(settings): Extension<Settings>) -> Response {
let mut request_builder = auth_state.oauth_client.authorize_url(CsrfToken::new_random);
for scope in settings.scopes {
request_builder = request_builder.add_scope(oauth2::Scope::new(scope));
}
let (auth_url, csrf_token) = request_builder.url();
if !auth_state.store_csrf_token(csrf_token).await {
// Generated a CSRF token that already existed... this is bad
return (StatusCode::INTERNAL_SERVER_ERROR, "internal error: duplicate state, try again").into_response();
}
// Redirect to oauth2 provider
Redirect::to(&auth_url.to_string()).into_response()
}
// Valid user session required. If there is none, redirect to the auth page
async fn protected(user: User) -> impl IntoResponse {
format!(
"Welcome to the protected area :)\nHere's your info:\n{:?}",
user
)
}
async fn logout(
Extension(store): Extension<MemoryStore>,
TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> impl IntoResponse {
let cookie = cookies.get(COOKIE_NAME).unwrap();
let session = match store.load_session(cookie.to_string()).await.unwrap() {
Some(s) => s,
// No session active, just redirect
None => return Redirect::to("/"),
};
store.destroy_session(session).await.unwrap();
Redirect::to("/")
}
#[derive(Debug, Deserialize)]
struct AuthRequest {
code: String,
state: String,
}
async fn login_authorized(
Query(query): Query<AuthRequest>,
Extension(store): Extension<MemoryStore>,
Extension(auth_state): Extension<AuthState>,
Extension(settings): Extension<Settings>,
) -> Response {
if !auth_state.validate_csrf_token(&query.state).await {
// the auth request state isn't valid, reject this attempt.
return (StatusCode::FORBIDDEN, "auth request state isn't valid, try again").into_response();
}
// Get an auth token
let token = auth_state.oauth_client
.exchange_code(AuthorizationCode::new(query.code.clone()))
.request_async(async_http_client)
.await
.unwrap();
// Fetch user data from oauth provider
let client = reqwest::Client::new();
let user_data: User = client
.get(settings.userinfo_url)
.bearer_auth(token.access_token().secret())
.send()
.await
.unwrap()
.json::<User>()
.await
.unwrap();
// Create a new session filled with user data
let mut session = Session::new();
session.insert("user", &user_data).unwrap();
// Store session and get corresponding cookie
let cookie = store.store_session(session).await.unwrap().unwrap();
// Build the cookie
let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie);
// Set cookie
let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, cookie.parse().unwrap());
(headers, Redirect::to("/")).into_response()
}
struct AuthRedirect;
impl IntoResponse for AuthRedirect {
fn into_response(self) -> Response {
Redirect::temporary("/login").into_response()
}
}
#[async_trait]
impl<B> FromRequest<B> for User
where
B: Send,
{
// If anything goes wrong or no session is found, redirect to the auth page
type Rejection = AuthRedirect;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
let Extension(store) = Extension::<MemoryStore>::from_request(req)
.await
.expect("`MemoryStore` extension is missing");
let cookies = TypedHeader::<headers::Cookie>::from_request(req)
.await
.map_err(|e| match *e.name() {
header::COOKIE => match e.reason() {
TypedHeaderRejectionReason::Missing => AuthRedirect,
_ => panic!("unexpected error getting Cookie header(s): {}", e),
},
_ => panic!("unexpected error getting cookies: {}", e),
})?;
let session_cookie = cookies.get(COOKIE_NAME).ok_or(AuthRedirect)?;
let session = store
.load_session(session_cookie.to_string())
.await
.unwrap()
.ok_or(AuthRedirect)?;
let user = session.get::<User>("user").ok_or(AuthRedirect)?;
Ok(user)
}
}

83
server/src/schema.rs Normal file
View File

@ -0,0 +1,83 @@
// @generated automatically by Diesel CLI.
diesel::table! {
approvals (id) {
id -> Int4,
description -> Varchar,
approved -> Bool,
}
}
diesel::table! {
boards (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Varchar>,
}
}
diesel::table! {
posts (id) {
id -> Int4,
text -> Varchar,
author_id -> Nullable<Int4>,
thread_id -> Nullable<Int4>,
reply_to -> Nullable<Int4>,
}
}
diesel::table! {
profiles (id) {
id -> Int4,
display_name -> Varchar,
user_id -> Nullable<Int4>,
}
}
diesel::table! {
reactions (id) {
id -> Int4,
content -> Varchar,
author_id -> Nullable<Int4>,
post_id -> Nullable<Int4>,
}
}
diesel::table! {
threads (id) {
id -> Int4,
title -> Varchar,
author_id -> Nullable<Int4>,
board_id -> Nullable<Int4>,
locked -> Bool,
sticky -> Bool,
}
}
diesel::table! {
users (id) {
id -> Int4,
external_user_id -> Varchar,
active -> Bool,
approval_id -> Nullable<Int4>,
}
}
diesel::joinable!(posts -> profiles (author_id));
diesel::joinable!(posts -> threads (thread_id));
diesel::joinable!(profiles -> users (user_id));
diesel::joinable!(reactions -> posts (post_id));
diesel::joinable!(reactions -> profiles (author_id));
diesel::joinable!(threads -> boards (board_id));
diesel::joinable!(threads -> profiles (author_id));
diesel::joinable!(users -> approvals (approval_id));
diesel::allow_tables_to_appear_in_same_query!(
approvals,
boards,
posts,
profiles,
reactions,
threads,
users,
);