start extending bespoke into another *forum*
This commit is contained in:
parent
b1d0cef3c9
commit
c1fd43389b
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
*/result
|
||||
*/bespoke.toml
|
||||
*/.env
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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"
|
|
@ -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,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,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;
|
|
@ -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)
|
||||
);
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
Loading…
Reference in New Issue