diff --git a/.gitignore b/.gitignore index ea8c4bf..f5f83f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +debug/ diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 80f74b8..5ddab89 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -198,10 +198,11 @@ pub(crate) async fn replace_in_html( let mut input = if !lisp { input.to_string() } else { + let parsed = bberry::parse(input); if let Some(plugins) = plugins { - bberry::parse(input).render(plugins) + parsed.render(plugins) } else { - bberry::parse(input).render_safe() + parsed.render_safe() } }; diff --git a/crates/app/src/public/html/misc/markdown.lisp b/crates/app/src/public/html/misc/markdown.lisp index 2296fcd..18ee05e 100644 --- a/crates/app/src/public/html/misc/markdown.lisp +++ b/crates/app/src/public/html/misc/markdown.lisp @@ -10,7 +10,7 @@ ("class" "card small flex items-center justify-between gap-2") (span ("class" "flex items-center gap-2") - (text "{{ icon scroll-text }}") + (icon (text "scroll-text")) (span (text "{{ file_name }}")))) (div diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js index 0778587..18099e0 100644 --- a/crates/app/src/public/js/me.js +++ b/crates/app/src/public/js/me.js @@ -582,7 +582,7 @@ method: "POST", }) .then((res) => res.json()) - .then(async (res) => { + .then(async (_) => { // create challenge and store const verifier = await trigger("connections::pkce_verifier", [ 128, diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 530a57b..fe89a30 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -3,6 +3,7 @@ use super::*; use crate::cache::Cache; use crate::model::auth::UserConnections; use crate::model::moderation::AuditLogEntry; +use crate::model::oauth::AuthGrant; use crate::model::{ Error, Result, auth::{Token, User, UserSettings}, @@ -47,6 +48,7 @@ impl DataManager { request_count: get!(x->16(i32)) as usize, connections: serde_json::from_str(&get!(x->17(String)).to_string()).unwrap(), stripe_id: get!(x->18(String)), + grants: serde_json::from_str(&get!(x->19(String)).to_string()).unwrap(), } } @@ -103,6 +105,40 @@ impl DataManager { Ok(res.unwrap()) } + /// Get a user given just their grant token. + /// + /// Also returns the auth grant this token is associated with from the user. + /// + /// # Arguments + /// * `token` - the token of the user + pub async fn get_user_by_grant_token(&self, token: &str) -> Result<(AuthGrant, User)> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_row!( + &conn, + "SELECT * FROM users WHERE grants::jsonb @> ('{\"token\":' || $1 || '}')::jsonb", + &[&token], + |x| Ok(Self::get_user_from_row(x)) + ); + + if res.is_err() { + return Err(Error::UserNotFound); + } + + let user = res.unwrap(); + Ok(( + user.grants + .iter() + .find(|x| x.token == token) + .unwrap() + .clone(), + user, + )) + } + /// Create a new user in the database. /// /// # Arguments @@ -696,6 +732,7 @@ impl DataManager { } auto_method!(update_user_tokens(Vec)@get_user_by_id -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); + auto_method!(update_user_grants(Vec)@get_user_by_id -> "UPDATE users SET grants = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_settings(UserSettings)@get_user_by_id -> "UPDATE users SET settings = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_connections(UserConnections)@get_user_by_id -> "UPDATE users SET connections = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_subscriptions(HashMap)@get_user_by_id -> "UPDATE users SET subscriptions = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 9cd761b..2b386b1 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use super::permissions::FinePermission; +use super::{oauth::AuthGrant, permissions::FinePermission}; use serde::{Deserialize, Serialize}; use totp_rs::TOTP; use tetratto_shared::{ @@ -43,6 +43,9 @@ pub struct User { /// The user's Stripe customer ID. #[serde(default)] pub stripe_id: String, + /// The grants associated with the user's account. + #[serde(default)] + pub grants: Vec, } pub type UserConnections = @@ -251,6 +254,7 @@ impl User { request_count: 0, connections: HashMap::new(), stripe_id: String::new(), + grants: Vec::new(), } } diff --git a/crates/core/src/model/oauth.rs b/crates/core/src/model/oauth.rs index 46bde81..4f08cd6 100644 --- a/crates/core/src/model/oauth.rs +++ b/crates/core/src/model/oauth.rs @@ -3,21 +3,63 @@ use serde::{Serialize, Deserialize}; use tetratto_shared::hash::hash; use super::{Result, Error}; -#[derive(Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthGrant { + pub id: usize, + /// The name of the application associated with this grant. + pub name: String, + /// The code challenge for PKCE verifiers associated with this grant. + /// + /// This challenge is *all* that is required to refresh this grant's auth token. + /// While there can only be one token at a time, it can be refreshed whenever as long + /// as the provided verifier matches that of the challenge. + /// + /// The challenge should never be changed. To change the challenge, the grant + /// should be removed and recreated. + pub challenge: String, + /// The encoding method for the initial verifier in the challenge. + pub method: PkceChallengeMethod, + /// The access token associated with the account. This is **not** the same as + /// regular account access tokens, as the token can only be used with the requested `scopes`. + pub token: String, + /// Scopes define what the grant's token is actually allowed to do. + /// + /// No scope shall ever be allowed to change scopes or manage grants on behalf of the user. + /// A regular user token **must** be provided to manage grants. + pub scopes: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum PkceChallengeMethod { S256, } -#[derive(Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum AppScope { + /// Read the user's profile (username, bio, etc). UserReadProfile, + /// Read the user's settings. + UserReadSettings, + /// Read the user's sessions and info. UserReadSessions, + /// Read posts as the user. UserReadPosts, + /// Read messages as the user. UserReadMessages, + /// Create posts as the user. UserCreatePosts, + /// Create messages as the user. UserCreateMessages, + /// Delete posts owned by the user. UserDeletePosts, + /// Delete messages owned by the user. UserDeleteMessages, + /// Manage stacks owned by the user. + UserManageStacks, + /// Manage the user's following/unfollowing. + UserManageRelationships, + /// Manage the user's settings. + UserManageSettings, } impl AppScope { @@ -27,6 +69,7 @@ impl AppScope { for scope in input.split(" ") { out.push(match scope { "user-read-profile" => Self::UserReadProfile, + "user-read-settings" => Self::UserReadSettings, "user-read-sessions" => Self::UserReadSessions, "user-read-posts" => Self::UserReadPosts, "user-read-messages" => Self::UserReadMessages, @@ -34,6 +77,9 @@ impl AppScope { "user-create-messages" => Self::UserCreateMessages, "user-delete-posts" => Self::UserDeletePosts, "user-delete-messages" => Self::UserDeleteMessages, + "user-manage-stacks" => Self::UserManageStacks, + "user-manage-relationships" => Self::UserManageRelationships, + "user-manage-settings" => Self::UserManageSettings, _ => continue, }) } diff --git a/sql_changes/users_grants.sql b/sql_changes/users_grants.sql new file mode 100644 index 0000000..8dbe154 --- /dev/null +++ b/sql_changes/users_grants.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN grants TEXT NOT NULL DEFAULT '[]';