forked from 2sys/shoutdotdev
misc back-end cleanup
This commit is contained in:
parent
9b4c8058d6
commit
47bb893d3f
18 changed files with 366 additions and 301 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -227,6 +227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core 0.5.0",
|
"axum-core 0.5.0",
|
||||||
|
"axum-macros",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -317,6 +318,17 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-macros"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.74"
|
version = "0.3.74"
|
||||||
|
|
|
@ -27,7 +27,7 @@ tracing-subscriber = { version = "0.3.19", features = ["chrono", "env-filter"] }
|
||||||
tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "fs", "trace", "tracing"] }
|
tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "fs", "trace", "tracing"] }
|
||||||
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "tracing"] }
|
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "tracing"] }
|
||||||
deadpool-diesel = { version = "0.6.1", features = ["postgres", "serde"] }
|
deadpool-diesel = { version = "0.6.1", features = ["postgres", "serde"] }
|
||||||
axum = "0.8.1"
|
axum = { version = "0.8.1", features = ["macros"] }
|
||||||
axum-extra = { version = "0.10.0", features = ["cookie", "typed-header"] }
|
axum-extra = { version = "0.10.0", features = ["cookie", "typed-header"] }
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
|
|
@ -8,9 +8,9 @@ CREATE INDEX ON users (uid);
|
||||||
CREATE TABLE IF NOT EXISTS csrf_tokens (
|
CREATE TABLE IF NOT EXISTS csrf_tokens (
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
user_id UUID REFERENCES users(id),
|
user_id UUID REFERENCES users(id),
|
||||||
expires_at TIMESTAMPTZ NOT NULL
|
created_at TIMESTAMPTZ NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX ON csrf_tokens (expires_at);
|
CREATE INDEX ON csrf_tokens (created_at);
|
||||||
|
|
||||||
CREATE TABLE teams (
|
CREATE TABLE teams (
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
CREATE TABLE browser_sessions (
|
CREATE TABLE browser_sessions (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
serialized TEXT NOT NULL
|
serialized TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_seen_at TIMESTAMPTZ NOT NULL
|
||||||
);
|
);
|
||||||
|
CREATE INDEX ON browser_sessions (last_seen_at);
|
||||||
|
CREATE INDEX ON browser_sessions (created_at);
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use anyhow::Result;
|
use deadpool_diesel::postgres::Connection;
|
||||||
use deadpool_diesel::postgres::Pool;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{app_error::AppError, models::Team, schema};
|
use crate::{app_error::AppError, schema::api_keys::dsl::*, teams::Team};
|
||||||
|
|
||||||
#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
|
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = schema::api_keys)]
|
#[diesel(table_name = crate::schema::api_keys)]
|
||||||
#[diesel(belongs_to(Team))]
|
#[diesel(belongs_to(Team))]
|
||||||
pub struct ApiKey {
|
pub struct ApiKey {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -14,19 +13,20 @@ pub struct ApiKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiKey {
|
impl ApiKey {
|
||||||
pub async fn generate_for_team(db_pool: &Pool, team_id: Uuid) -> Result<Self, AppError> {
|
pub async fn generate_for_team(
|
||||||
let id = Uuid::new_v4();
|
db_conn: &Connection,
|
||||||
let api_key = db_pool
|
key_team_id: Uuid,
|
||||||
.get()
|
) -> Result<Self, AppError> {
|
||||||
.await?
|
let api_key = Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
team_id: key_team_id,
|
||||||
|
};
|
||||||
|
let api_key_copy = api_key.clone();
|
||||||
|
db_conn
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
diesel::insert_into(schema::api_keys::table)
|
diesel::insert_into(api_keys)
|
||||||
.values((
|
.values(api_key_copy)
|
||||||
schema::api_keys::id.eq(id),
|
.execute(conn)
|
||||||
schema::api_keys::team_id.eq(team_id),
|
|
||||||
))
|
|
||||||
.returning(ApiKey::as_select())
|
|
||||||
.get_result(conn)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use axum::extract::FromRef;
|
use axum::{
|
||||||
use deadpool_diesel::postgres::Pool;
|
extract::{FromRef, FromRequestParts},
|
||||||
|
http::request::Parts,
|
||||||
|
};
|
||||||
|
use deadpool_diesel::postgres::{Connection, Pool};
|
||||||
use oauth2::basic::BasicClient;
|
use oauth2::basic::BasicClient;
|
||||||
|
|
||||||
use crate::{sessions::PgStore, settings::Settings};
|
use crate::{app_error::AppError, sessions::PgStore, settings::Settings};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct AppState {
|
pub(crate) struct AppState {
|
||||||
|
@ -17,3 +20,14 @@ impl FromRef<AppState> for PgStore {
|
||||||
state.session_store.clone()
|
state.session_store.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DbConn(pub Connection);
|
||||||
|
|
||||||
|
impl FromRequestParts<AppState> for DbConn {
|
||||||
|
type Rejection = AppError;
|
||||||
|
|
||||||
|
async fn from_request_parts(_: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||||
|
let conn = state.db_pool.get().await?;
|
||||||
|
Ok(Self(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
96
src/csrf.rs
96
src/csrf.rs
|
@ -1,60 +1,84 @@
|
||||||
use anyhow::{Context, Result};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use chrono::{TimeDelta, Utc};
|
use deadpool_diesel::postgres::Connection;
|
||||||
use deadpool_diesel::postgres::Pool;
|
use diesel::{
|
||||||
use diesel::prelude::*;
|
dsl::{AsSelect, Eq, Gt, IsNotDistinctFrom, Select},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{app_error::AppError, models::CsrfToken, schema};
|
use crate::{app_error::AppError, schema::csrf_tokens::dsl::*};
|
||||||
|
|
||||||
const TOKEN_PREFIX: &'static str = "csrf__";
|
const TOKEN_PREFIX: &'static str = "csrf-";
|
||||||
const TTL_SEC: i64 = 60 * 60 * 24 * 7;
|
|
||||||
|
|
||||||
pub async fn generate_csrf_token_for_user(
|
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
|
||||||
db_pool: &Pool,
|
#[diesel(table_name = crate::schema::csrf_tokens)]
|
||||||
uid: Option<Uuid>,
|
#[diesel(check_for_backend(Pg))]
|
||||||
|
pub struct CsrfToken {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub user_id: Option<Uuid>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsrfToken {
|
||||||
|
fn all() -> Select<csrf_tokens, AsSelect<CsrfToken, Pg>> {
|
||||||
|
csrf_tokens.select(Self::as_select())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_not_expired() -> Gt<created_at, DateTime<Utc>> {
|
||||||
|
let ttl = TimeDelta::hours(24);
|
||||||
|
let min_created_at: DateTime<Utc> = Utc::now() - ttl;
|
||||||
|
created_at.gt(min_created_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_user_id(token_user_id: Option<Uuid>) -> IsNotDistinctFrom<user_id, Option<Uuid>> {
|
||||||
|
user_id.is_not_distinct_from(token_user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_token_id(token_id: Uuid) -> Eq<id, Uuid> {
|
||||||
|
id.eq(token_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_csrf_token(
|
||||||
|
db_conn: &Connection,
|
||||||
|
with_user_id: Option<Uuid>,
|
||||||
) -> Result<String, AppError> {
|
) -> Result<String, AppError> {
|
||||||
let id = Uuid::new_v4();
|
let token_id = Uuid::new_v4();
|
||||||
let expires_at =
|
db_conn
|
||||||
Utc::now() + TimeDelta::new(TTL_SEC, 0).context("Failed to generate TimeDelta")?;
|
|
||||||
db_pool
|
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
diesel::insert_into(schema::csrf_tokens::table)
|
diesel::insert_into(csrf_tokens)
|
||||||
.values((
|
.values((
|
||||||
schema::csrf_tokens::id.eq(id),
|
id.eq(token_id),
|
||||||
schema::csrf_tokens::user_id.eq(uid),
|
user_id.eq(with_user_id),
|
||||||
schema::csrf_tokens::expires_at.eq(expires_at),
|
created_at.eq(diesel::dsl::now),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
Ok(format!("{}{}", TOKEN_PREFIX, id.hyphenated().to_string()))
|
Ok(format!("{}{}", TOKEN_PREFIX, token_id.simple().to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn validate_csrf_token_for_user(
|
pub async fn validate_csrf_token(
|
||||||
db_pool: &Pool,
|
db_conn: &Connection,
|
||||||
token: &str,
|
token: &str,
|
||||||
uid: Option<Uuid>,
|
with_user_id: Option<Uuid>,
|
||||||
) -> Result<bool, AppError> {
|
) -> Result<bool, AppError> {
|
||||||
let id = match Uuid::try_parse(&token[TOKEN_PREFIX.len()..]) {
|
let token_id = match Uuid::try_parse(&token[TOKEN_PREFIX.len()..]) {
|
||||||
Ok(id) => id,
|
Ok(token_id) => token_id,
|
||||||
Err(_) => return Ok(false),
|
Err(_) => return Ok(false),
|
||||||
};
|
};
|
||||||
let row = db_pool
|
Ok(db_conn
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
schema::csrf_tokens::table
|
CsrfToken::all()
|
||||||
.select(CsrfToken::as_select())
|
.filter(CsrfToken::with_token_id(token_id))
|
||||||
.filter(schema::csrf_tokens::id.eq(id))
|
.filter(CsrfToken::with_user_id(with_user_id))
|
||||||
.filter(schema::csrf_tokens::expires_at.gt(Utc::now()))
|
.filter(CsrfToken::is_not_expired())
|
||||||
.filter(schema::csrf_tokens::user_id.is_not_distinct_from(uid))
|
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?
|
||||||
Ok(row.is_some())
|
.is_some())
|
||||||
}
|
}
|
||||||
|
|
45
src/guards.rs
Normal file
45
src/guards.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
macro_rules! require_team_membership {
|
||||||
|
($current_user:expr, $team_id:expr, $db_conn:expr) => {{
|
||||||
|
let current_user_id = $current_user.id.clone();
|
||||||
|
match $db_conn
|
||||||
|
.interact(move |conn| {
|
||||||
|
crate::team_memberships::TeamMembership::all()
|
||||||
|
.filter(crate::team_memberships::TeamMembership::with_user_id(
|
||||||
|
current_user_id,
|
||||||
|
))
|
||||||
|
.filter(crate::team_memberships::TeamMembership::with_team_id(
|
||||||
|
$team_id,
|
||||||
|
))
|
||||||
|
.first(conn)
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()?
|
||||||
|
{
|
||||||
|
Some((team, _)) => team,
|
||||||
|
None => {
|
||||||
|
return Ok((
|
||||||
|
axum::http::StatusCode::FORBIDDEN,
|
||||||
|
"not a member of requested team".to_string(),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
pub(crate) use require_team_membership;
|
||||||
|
|
||||||
|
macro_rules! require_valid_csrf_token {
|
||||||
|
($csrf_token:expr, $current_user:expr, $db_conn:expr) => {{
|
||||||
|
if !crate::csrf::validate_csrf_token(&$db_conn, &$csrf_token, Some($current_user.id))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Ok((
|
||||||
|
axum::http::StatusCode::FORBIDDEN,
|
||||||
|
"invalid CSRF token".to_string(),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
pub(crate) use require_valid_csrf_token;
|
|
@ -3,20 +3,19 @@ mod app_error;
|
||||||
mod app_state;
|
mod app_state;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod csrf;
|
mod csrf;
|
||||||
mod models;
|
mod guards;
|
||||||
mod projects;
|
mod projects;
|
||||||
mod router;
|
mod router;
|
||||||
mod schema;
|
mod schema;
|
||||||
mod sessions;
|
mod sessions;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
mod team_memberships;
|
||||||
|
mod teams;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
use app_state::AppState;
|
|
||||||
use router::new_router;
|
|
||||||
use sessions::PgStore;
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use crate::settings::Settings;
|
use crate::{app_state::AppState, router::new_router, sessions::PgStore, settings::Settings};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
|
|
@ -1,42 +1 @@
|
||||||
use chrono::{offset::Utc, DateTime};
|
|
||||||
use diesel::{pg::Pg, prelude::*};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::schema;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
|
||||||
#[diesel(table_name = schema::teams)]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct Team {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
|
||||||
#[diesel(table_name = schema::team_memberships)]
|
|
||||||
#[diesel(belongs_to(Team))]
|
|
||||||
#[diesel(belongs_to(crate::users::User))]
|
|
||||||
#[diesel(primary_key(team_id, user_id))]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct TeamMembership {
|
|
||||||
pub team_id: Uuid,
|
|
||||||
pub user_id: Uuid,
|
|
||||||
pub roles: Vec<Option<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
|
|
||||||
#[diesel(table_name = schema::browser_sessions)]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct BrowserSession {
|
|
||||||
pub id: String,
|
|
||||||
pub serialized: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
|
|
||||||
#[diesel(table_name = schema::csrf_tokens)]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct CsrfToken {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub user_id: Option<Uuid>,
|
|
||||||
pub expires_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
use diesel::prelude::*;
|
use diesel::{
|
||||||
|
dsl::{auto_type, AsSelect},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{models::Team, schema};
|
use crate::{
|
||||||
|
schema::{self, projects::dsl::*},
|
||||||
|
teams::Team,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = schema::projects)]
|
#[diesel(table_name = schema::projects)]
|
||||||
|
@ -11,3 +18,11 @@ pub struct Project {
|
||||||
pub team_id: Uuid,
|
pub team_id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Project {
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn all() -> _ {
|
||||||
|
let select: AsSelect<Project, Pg> = Project::as_select();
|
||||||
|
projects.select(select)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
190
src/router.rs
190
src/router.rs
|
@ -1,13 +1,11 @@
|
||||||
use anyhow::anyhow;
|
|
||||||
use askama_axum::Template;
|
use askama_axum::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::status::StatusCode,
|
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, IntoResponse, Redirect},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Form, Router,
|
Form, Router,
|
||||||
};
|
};
|
||||||
use diesel::{dsl::insert_into, prelude::*, result::Error::NotFound};
|
use diesel::{dsl::insert_into, prelude::*};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::{
|
use tower_http::{
|
||||||
|
@ -20,12 +18,15 @@ use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
api_keys::ApiKey,
|
api_keys::ApiKey,
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
app_state::AppState,
|
app_state::{AppState, DbConn},
|
||||||
auth::{self, AuthInfo},
|
auth,
|
||||||
csrf::{generate_csrf_token_for_user, validate_csrf_token_for_user},
|
csrf::generate_csrf_token,
|
||||||
models::{Team, TeamMembership},
|
guards,
|
||||||
projects::Project,
|
projects::Project,
|
||||||
schema,
|
schema,
|
||||||
|
settings::Settings,
|
||||||
|
team_memberships::TeamMembership,
|
||||||
|
teams::Team,
|
||||||
users::{CurrentUser, User},
|
users::{CurrentUser, User},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,14 +60,12 @@ async fn landing_page(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn teams_page(
|
async fn teams_page(
|
||||||
State(state): State<AppState>,
|
State(Settings { base_path, .. }): State<Settings>,
|
||||||
|
DbConn(conn): DbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let current_user_id = current_user.id.clone();
|
let current_user_id = current_user.id.clone();
|
||||||
let teams_of_current_user = state
|
let teams_of_current_user = conn
|
||||||
.db_pool
|
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
schema::team_memberships::table
|
schema::team_memberships::table
|
||||||
.inner_join(schema::teams::table)
|
.inner_join(schema::teams::table)
|
||||||
|
@ -75,8 +74,7 @@ async fn teams_page(
|
||||||
.load(conn)
|
.load(conn)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()?;
|
||||||
.unwrap();
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "teams.html")]
|
#[template(path = "teams.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
@ -86,13 +84,12 @@ async fn teams_page(
|
||||||
}
|
}
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
|
base_path,
|
||||||
current_user,
|
current_user,
|
||||||
base_path: state.settings.base_path,
|
|
||||||
teams: teams_of_current_user,
|
teams: teams_of_current_user,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
)
|
))
|
||||||
.into_response())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn team_page(State(state): State<AppState>, Path(team_id): Path<Uuid>) -> impl IntoResponse {
|
async fn team_page(State(state): State<AppState>, Path(team_id): Path<Uuid>) -> impl IntoResponse {
|
||||||
|
@ -108,76 +105,30 @@ struct PostNewApiKeyForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_new_api_key(
|
async fn post_new_api_key(
|
||||||
State(state): State<AppState>,
|
State(Settings { base_path, .. }): State<Settings>,
|
||||||
|
DbConn(db_conn): DbConn,
|
||||||
Path(team_id): Path<Uuid>,
|
Path(team_id): Path<Uuid>,
|
||||||
user_info: AuthInfo,
|
CurrentUser(current_user): CurrentUser,
|
||||||
Form(form): Form<PostNewApiKeyForm>,
|
Form(form): Form<PostNewApiKeyForm>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let current_uid = user_info.sub.clone();
|
guards::require_valid_csrf_token!(form.csrf_token, current_user, db_conn);
|
||||||
let current_user = state
|
let team = guards::require_team_membership!(current_user, team_id, db_conn);
|
||||||
.db_pool
|
|
||||||
.get()
|
ApiKey::generate_for_team(&db_conn, team.id.clone()).await?;
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
|
||||||
schema::users::table
|
|
||||||
.filter(schema::users::uid.eq(current_uid))
|
|
||||||
.select(User::as_select())
|
|
||||||
.first(conn)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap() // Bubble up panic from callback
|
|
||||||
.map_err(|diesel_err| match diesel_err {
|
|
||||||
NotFound => AppError::InternalServerError(anyhow!(
|
|
||||||
"user not found in database for uid {}",
|
|
||||||
user_info.sub
|
|
||||||
)),
|
|
||||||
_ => AppError::InternalServerError(diesel_err.into()),
|
|
||||||
})?;
|
|
||||||
if !validate_csrf_token_for_user(&state.db_pool, &form.csrf_token, Some(current_user.id))
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
return Ok((StatusCode::FORBIDDEN, "invalid CSRF token".to_string()).into_response());
|
|
||||||
}
|
|
||||||
let team_membership = match state
|
|
||||||
.db_pool
|
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
|
||||||
schema::team_memberships::table
|
|
||||||
.filter(schema::team_memberships::team_id.eq(team_id))
|
|
||||||
.filter(schema::team_memberships::user_id.eq(current_user.id))
|
|
||||||
.select(TeamMembership::as_select())
|
|
||||||
.first(conn)
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Some(team_membership) => team_membership,
|
|
||||||
None => {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::FORBIDDEN,
|
|
||||||
"not a member of requested team".to_string(),
|
|
||||||
)
|
|
||||||
.into_response());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ApiKey::generate_for_team(&state.db_pool, team_membership.team_id.clone()).await?;
|
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"{}/teams/{}/projects",
|
"{}/teams/{}/projects",
|
||||||
state.settings.base_path,
|
base_path,
|
||||||
team_membership.team_id.hyphenated().to_string()
|
team.id.hyphenated().to_string()
|
||||||
))
|
))
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn new_team_page(
|
async fn new_team_page(
|
||||||
State(state): State<AppState>,
|
State(Settings { base_path, .. }): State<Settings>,
|
||||||
|
DbConn(db_conn): DbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let csrf_token =
|
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
||||||
generate_csrf_token_for_user(&state.db_pool, Some(current_user.id.clone())).await?;
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "new-team.html")]
|
#[template(path = "new-team.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
@ -187,9 +138,9 @@ async fn new_team_page(
|
||||||
}
|
}
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
|
base_path,
|
||||||
csrf_token,
|
csrf_token,
|
||||||
current_user,
|
current_user,
|
||||||
base_path: state.settings.base_path,
|
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
))
|
))
|
||||||
|
@ -202,33 +153,13 @@ struct PostNewTeamForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_new_team(
|
async fn post_new_team(
|
||||||
State(state): State<AppState>,
|
DbConn(db_conn): DbConn,
|
||||||
user_info: AuthInfo,
|
State(Settings { base_path, .. }): State<Settings>,
|
||||||
|
CurrentUser(current_user): CurrentUser,
|
||||||
Form(form): Form<PostNewTeamForm>,
|
Form(form): Form<PostNewTeamForm>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let current_uid = user_info.sub.clone();
|
guards::require_valid_csrf_token!(form.csrf_token, current_user, db_conn);
|
||||||
let current_user = state
|
|
||||||
.db_pool
|
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
|
||||||
schema::users::table
|
|
||||||
.filter(schema::users::uid.eq(current_uid))
|
|
||||||
.select(User::as_select())
|
|
||||||
.first(conn)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
if !validate_csrf_token_for_user(
|
|
||||||
&state.db_pool,
|
|
||||||
&form.csrf_token,
|
|
||||||
Some(current_user.id.clone()),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
return Err(anyhow!("Invalid CSRF token").into());
|
|
||||||
}
|
|
||||||
let team_id = Uuid::now_v7();
|
let team_id = Uuid::now_v7();
|
||||||
let team = Team {
|
let team = Team {
|
||||||
id: team_id.clone(),
|
id: team_id.clone(),
|
||||||
|
@ -239,10 +170,7 @@ async fn post_new_team(
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
roles: vec![Some("OWNER".to_string())],
|
roles: vec![Some("OWNER".to_string())],
|
||||||
};
|
};
|
||||||
state
|
db_conn
|
||||||
.db_pool
|
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
conn.transaction(move |conn| {
|
conn.transaction(move |conn| {
|
||||||
insert_into(schema::teams::table)
|
insert_into(schema::teams::table)
|
||||||
|
@ -257,50 +185,20 @@ async fn post_new_team(
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
ApiKey::generate_for_team(&state.db_pool, team_id.clone()).await?;
|
ApiKey::generate_for_team(&db_conn, team_id.clone()).await?;
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!("{}/teams/{}/projects", base_path, team_id)).into_response())
|
||||||
"{}/teams/{}/projects",
|
|
||||||
state.settings.base_path, team_id
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn projects_page(
|
async fn projects_page(
|
||||||
State(state): State<AppState>,
|
State(Settings { base_path, .. }): State<Settings>,
|
||||||
|
DbConn(db_conn): DbConn,
|
||||||
Path(team_id): Path<Uuid>,
|
Path(team_id): Path<Uuid>,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let current_user_id = current_user.id.clone();
|
let team = guards::require_team_membership!(current_user, team_id, db_conn);
|
||||||
let team = match state
|
|
||||||
.db_pool
|
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
|
||||||
schema::team_memberships::table
|
|
||||||
.inner_join(schema::teams::table)
|
|
||||||
.filter(schema::team_memberships::user_id.eq(current_user_id))
|
|
||||||
.filter(schema::teams::id.eq(team_id))
|
|
||||||
.select(Team::as_select())
|
|
||||||
.first(conn)
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Some(team) => team,
|
|
||||||
None => {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::FORBIDDEN,
|
|
||||||
"not a member of requested team".to_string(),
|
|
||||||
)
|
|
||||||
.into_response());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let team_id = team.id.clone();
|
let team_id = team.id.clone();
|
||||||
let api_keys = state
|
let api_keys = db_conn
|
||||||
.db_pool
|
|
||||||
.get()
|
|
||||||
.await?
|
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
schema::api_keys::table
|
schema::api_keys::table
|
||||||
.filter(schema::api_keys::team_id.eq(team_id))
|
.filter(schema::api_keys::team_id.eq(team_id))
|
||||||
|
@ -308,8 +206,7 @@ async fn projects_page(
|
||||||
.load(conn)
|
.load(conn)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()?;
|
||||||
.unwrap();
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "projects.html")]
|
#[template(path = "projects.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
@ -320,14 +217,13 @@ async fn projects_page(
|
||||||
team: Team,
|
team: Team,
|
||||||
current_user: User,
|
current_user: User,
|
||||||
}
|
}
|
||||||
let csrf_token =
|
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id.clone())).await?;
|
||||||
generate_csrf_token_for_user(&state.db_pool, Some(current_user.id.clone())).await?;
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
|
base_path,
|
||||||
csrf_token,
|
csrf_token,
|
||||||
current_user,
|
current_user,
|
||||||
team,
|
team,
|
||||||
base_path: state.settings.base_path,
|
|
||||||
keys: api_keys,
|
keys: api_keys,
|
||||||
projects: vec![],
|
projects: vec![],
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ diesel::table! {
|
||||||
browser_sessions (id) {
|
browser_sessions (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
serialized -> Text,
|
serialized -> Text,
|
||||||
|
created_at -> Timestamptz,
|
||||||
|
last_seen_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +20,7 @@ diesel::table! {
|
||||||
csrf_tokens (id) {
|
csrf_tokens (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
user_id -> Nullable<Uuid>,
|
user_id -> Nullable<Uuid>,
|
||||||
expires_at -> Timestamptz,
|
created_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_session::{async_trait, Session, SessionStore};
|
use async_session::{async_trait, Session, SessionStore};
|
||||||
use diesel::prelude::*;
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
|
use diesel::{pg::Pg, prelude::*, upsert::excluded};
|
||||||
|
|
||||||
use crate::{models::BrowserSession, schema};
|
use crate::schema::browser_sessions::dsl::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
|
||||||
|
#[diesel(table_name = crate::schema::browser_sessions)]
|
||||||
|
#[diesel(check_for_backend(Pg))]
|
||||||
|
pub struct BrowserSession {
|
||||||
|
pub id: String,
|
||||||
|
pub serialized: String,
|
||||||
|
pub last_seen_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PgStore {
|
pub struct PgStore {
|
||||||
// TODO: reference instead of clone
|
|
||||||
pool: deadpool_diesel::postgres::Pool,
|
pool: deadpool_diesel::postgres::Pool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +27,7 @@ impl PgStore {
|
||||||
|
|
||||||
impl std::fmt::Debug for PgStore {
|
impl std::fmt::Debug for PgStore {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "PgStore {{ pool }}")?;
|
write!(f, "PgStore")?;
|
||||||
Ok(()).into()
|
Ok(()).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,39 +35,47 @@ impl std::fmt::Debug for PgStore {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SessionStore for PgStore {
|
impl SessionStore for PgStore {
|
||||||
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
|
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
|
||||||
let conn = self.pool.get().await?;
|
|
||||||
let session_id = Session::id_from_cookie_value(&cookie_value)?;
|
let session_id = Session::id_from_cookie_value(&cookie_value)?;
|
||||||
let rows = conn
|
let timestamp_stale = Utc::now() - TimeDelta::days(7);
|
||||||
|
let conn = self.pool.get().await?;
|
||||||
|
let row = conn
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
schema::browser_sessions::table
|
// Drop all sessions without recent activity
|
||||||
.filter(schema::browser_sessions::id.eq(session_id))
|
diesel::delete(browser_sessions.filter(last_seen_at.lt(timestamp_stale)))
|
||||||
.select(BrowserSession::as_select())
|
.execute(conn)?;
|
||||||
.load(conn)
|
diesel::update(browser_sessions.filter(id.eq(session_id)))
|
||||||
|
.set(last_seen_at.eq(diesel::dsl::now))
|
||||||
|
.returning(BrowserSession::as_returning())
|
||||||
|
.get_result(conn)
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
if rows.len() == 0 {
|
Ok(match row {
|
||||||
Ok(None)
|
Some(session) => Some(serde_json::from_str::<Session>(
|
||||||
} else {
|
session.serialized.as_str(),
|
||||||
Ok(Some(serde_json::from_str::<Session>(
|
)?),
|
||||||
rows[0].serialized.as_str(),
|
None => None,
|
||||||
)?))
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn store_session(&self, session: Session) -> Result<Option<String>> {
|
async fn store_session(&self, session: Session) -> Result<Option<String>> {
|
||||||
let serialized = serde_json::to_string(&session)?;
|
let serialized_data = serde_json::to_string(&session)?;
|
||||||
let conn = self.pool.get().await?;
|
|
||||||
let session_id = session.id().to_string();
|
let session_id = session.id().to_string();
|
||||||
|
let conn = self.pool.get().await?;
|
||||||
conn.interact(move |conn| {
|
conn.interact(move |conn| {
|
||||||
diesel::insert_into(schema::browser_sessions::table)
|
diesel::insert_into(browser_sessions)
|
||||||
.values((
|
.values((
|
||||||
schema::browser_sessions::id.eq(session_id),
|
id.eq(session_id),
|
||||||
schema::browser_sessions::serialized.eq(serialized.clone()),
|
serialized.eq(serialized_data),
|
||||||
|
last_seen_at.eq(diesel::dsl::now),
|
||||||
))
|
))
|
||||||
.on_conflict(schema::browser_sessions::id)
|
.on_conflict(id)
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(schema::browser_sessions::serialized.eq(serialized.clone()))
|
.set((
|
||||||
|
serialized.eq(excluded(serialized)),
|
||||||
|
last_seen_at.eq(excluded(last_seen_at)),
|
||||||
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
@ -70,11 +87,7 @@ impl SessionStore for PgStore {
|
||||||
async fn destroy_session(&self, session: Session) -> Result<()> {
|
async fn destroy_session(&self, session: Session) -> Result<()> {
|
||||||
let conn = self.pool.get().await?;
|
let conn = self.pool.get().await?;
|
||||||
conn.interact(move |conn| {
|
conn.interact(move |conn| {
|
||||||
diesel::delete(
|
diesel::delete(browser_sessions.filter(id.eq(session.id().to_string()))).execute(conn)
|
||||||
schema::browser_sessions::table
|
|
||||||
.filter(schema::browser_sessions::id.eq(session.id().to_string())),
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
@ -83,7 +96,7 @@ impl SessionStore for PgStore {
|
||||||
|
|
||||||
async fn clear_store(&self) -> Result<()> {
|
async fn clear_store(&self) -> Result<()> {
|
||||||
let conn = self.pool.get().await?;
|
let conn = self.pool.get().await?;
|
||||||
conn.interact(move |conn| diesel::delete(schema::browser_sessions::table).execute(conn))
|
conn.interact(move |conn| diesel::delete(browser_sessions).execute(conn))
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
use axum::extract::FromRef;
|
||||||
use config::{Config, ConfigError, Environment};
|
use config::{Config, ConfigError, Environment};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -54,3 +57,9 @@ impl Settings {
|
||||||
s.try_deserialize()
|
s.try_deserialize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for Settings {
|
||||||
|
fn from_ref(state: &AppState) -> Self {
|
||||||
|
state.settings.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
43
src/team_memberships.rs
Normal file
43
src/team_memberships.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use diesel::{
|
||||||
|
dsl::{AsSelect, Eq},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
schema::{self, team_memberships::dsl::*},
|
||||||
|
teams::Team,
|
||||||
|
users::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
|
#[diesel(table_name = schema::team_memberships)]
|
||||||
|
#[diesel(belongs_to(crate::teams::Team))]
|
||||||
|
#[diesel(belongs_to(crate::users::User))]
|
||||||
|
#[diesel(primary_key(team_id, user_id))]
|
||||||
|
#[diesel(check_for_backend(Pg))]
|
||||||
|
pub struct TeamMembership {
|
||||||
|
pub team_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub roles: Vec<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TeamMembership {
|
||||||
|
#[diesel::dsl::auto_type(no_type_alias)]
|
||||||
|
pub fn all() -> _ {
|
||||||
|
let select: AsSelect<(Team, User), Pg> = <(Team, User)>::as_select();
|
||||||
|
team_memberships
|
||||||
|
.inner_join(schema::teams::table)
|
||||||
|
.inner_join(schema::users::table)
|
||||||
|
.select(select)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_team_id(team_id_value: Uuid) -> Eq<team_id, Uuid> {
|
||||||
|
team_id.eq(team_id_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_user_id(user_id_value: Uuid) -> Eq<user_id, Uuid> {
|
||||||
|
user_id.eq(user_id_value)
|
||||||
|
}
|
||||||
|
}
|
22
src/teams.rs
Normal file
22
src/teams.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use diesel::{
|
||||||
|
dsl::{AsSelect, Select},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::schema::teams::dsl::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
|
#[diesel(table_name = crate::schema::teams)]
|
||||||
|
#[diesel(check_for_backend(Pg))]
|
||||||
|
pub struct Team {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Team {
|
||||||
|
pub fn all() -> Select<teams, AsSelect<Team, Pg>> {
|
||||||
|
teams.select(Team::as_select())
|
||||||
|
}
|
||||||
|
}
|
38
src/users.rs
38
src/users.rs
|
@ -5,20 +5,19 @@ use axum::{
|
||||||
RequestPartsExt,
|
RequestPartsExt,
|
||||||
};
|
};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
associations::Identifiable, deserialize::Queryable, dsl::insert_into, pg::Pg, prelude::*,
|
associations::Identifiable,
|
||||||
|
deserialize::Queryable,
|
||||||
|
dsl::{insert_into, AsSelect, Eq, Select},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
Selectable,
|
Selectable,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{app_error::AppError, app_state::AppState, auth::AuthInfo, schema::users::dsl::*};
|
||||||
app_error::AppError,
|
|
||||||
app_state::AppState,
|
|
||||||
auth::AuthInfo,
|
|
||||||
schema::{self, users},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = schema::users)]
|
#[diesel(table_name = crate::schema::users)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -26,6 +25,16 @@ pub struct User {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn all() -> Select<users, AsSelect<User, Pg>> {
|
||||||
|
users.select(User::as_select())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_uid(uid_value: &str) -> Eq<uid, &str> {
|
||||||
|
uid.eq(uid_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CurrentUser(pub User);
|
pub struct CurrentUser(pub User);
|
||||||
|
|
||||||
|
@ -46,9 +55,8 @@ impl FromRequestParts<AppState> for CurrentUser {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| CurrentUserRejection::InternalServerError(err.into()))?
|
.map_err(|err| CurrentUserRejection::InternalServerError(err.into()))?
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
let maybe_current_user = users::table
|
let maybe_current_user = User::all()
|
||||||
.filter(users::uid.eq(auth_info.sub.clone()))
|
.filter(User::with_uid(&auth_info.sub))
|
||||||
.select(User::as_select())
|
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
if let Some(current_user) = maybe_current_user {
|
if let Some(current_user) = maybe_current_user {
|
||||||
|
@ -59,11 +67,11 @@ impl FromRequestParts<AppState> for CurrentUser {
|
||||||
uid: auth_info.sub,
|
uid: auth_info.sub,
|
||||||
email: auth_info.email,
|
email: auth_info.email,
|
||||||
};
|
};
|
||||||
insert_into(users::table)
|
insert_into(users)
|
||||||
.values(&new_user)
|
.values(new_user)
|
||||||
.returning(User::as_returning())
|
.on_conflict(uid)
|
||||||
.on_conflict(users::uid)
|
|
||||||
.do_nothing()
|
.do_nothing()
|
||||||
|
.returning(User::as_returning())
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
Loading…
Add table
Reference in a new issue