summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock2
-rw-r--r--crates/auth/Cargo.toml1
-rw-r--r--crates/auth/auth.toml1
-rw-r--r--crates/auth/migrations/20250723100947_user.sql19
-rw-r--r--crates/auth/migrations/20250725160900_session.sql8
-rw-r--r--crates/auth/migrations/20250725161014_token.sql9
-rw-r--r--crates/auth/src/cnfg.rs1
-rw-r--r--crates/auth/src/server/routes.rs15
-rw-r--r--crates/auth/src/server/routes/authorised.rs137
9 files changed, 183 insertions, 10 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 087cf48..3dd458b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -213,6 +213,7 @@ dependencies = [
"tower-sessions-sqlx-store",
"tracing",
"url",
+ "uuid",
]
[[package]]
@@ -3278,6 +3279,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
+ "serde",
"wasm-bindgen",
]
diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml
index ccd18f3..410c51e 100644
--- a/crates/auth/Cargo.toml
+++ b/crates/auth/Cargo.toml
@@ -32,6 +32,7 @@ tower-sessions-moka-store = "0.15.0"
tower-sessions-sqlx-store = { version = "0.15.0", features = ["postgres"] }
tracing.workspace = true
url.workspace = true
+uuid = { workspace = true, features = ["serde", "v7"] }
[dependencies.stack-up]
workspace = true
diff --git a/crates/auth/auth.toml b/crates/auth/auth.toml
index 4e5b263..17a696f 100644
--- a/crates/auth/auth.toml
+++ b/crates/auth/auth.toml
@@ -4,6 +4,7 @@ port = 1304
[misc.oauth]
session-lifespan = 3600 # seconds
+jwt-encoding-key = "secret"
[misc.oauth.discord]
# query param for provider
diff --git a/crates/auth/migrations/20250723100947_user.sql b/crates/auth/migrations/20250723100947_user.sql
index 440afc7..b5566fe 100644
--- a/crates/auth/migrations/20250723100947_user.sql
+++ b/crates/auth/migrations/20250723100947_user.sql
@@ -2,8 +2,19 @@
create table auth_user (
id uuid primary key,
email text unique not null,
- avatar text,
- description text,
- updated_at text,
- create_at timestamptz not null default now()
+ updated_at timestamptz not null default now(),
+ created_at timestamptz not null default now()
);
+
+create or replace function set_updated_at()
+returns trigger as $$
+begin
+ new.updated_at := now();
+ return new;
+end;
+$$ language plpgsql;
+
+create trigger trigger_set_updated_at
+before update on auth_user
+for each row
+execute function set_updated_at();
diff --git a/crates/auth/migrations/20250725160900_session.sql b/crates/auth/migrations/20250725160900_session.sql
new file mode 100644
index 0000000..c5e76dc
--- /dev/null
+++ b/crates/auth/migrations/20250725160900_session.sql
@@ -0,0 +1,8 @@
+-- Add migration script here
+create schema if not exists "tower_sessions";
+
+create table "tower_sessions"."session" (
+ id text primary key not null,
+ data bytea not null,
+ expiry_date timestamptz not null
+);
diff --git a/crates/auth/migrations/20250725161014_token.sql b/crates/auth/migrations/20250725161014_token.sql
new file mode 100644
index 0000000..68f476c
--- /dev/null
+++ b/crates/auth/migrations/20250725161014_token.sql
@@ -0,0 +1,9 @@
+-- Add migration script here
+create table token (
+ user_id uuid not null,
+ token text not null,
+ session_id text not null,
+ primary key (user_id, session_id),
+ foreign key (session_id) references "tower_sessions"."session"(id) on delete cascade,
+ foreign key (user_id) references auth_user(id) on delete cascade
+);
diff --git a/crates/auth/src/cnfg.rs b/crates/auth/src/cnfg.rs
index af7b0a0..c895d05 100644
--- a/crates/auth/src/cnfg.rs
+++ b/crates/auth/src/cnfg.rs
@@ -11,6 +11,7 @@ pub struct LocalConfig {
pub struct OauthConfig {
pub discord: OauthCredentials,
pub session_lifespan: u64,
+ pub jwt_encoding_key: String,
}
#[derive(Deserialize, Clone)]
diff --git a/crates/auth/src/server/routes.rs b/crates/auth/src/server/routes.rs
index 1ab012c..6773962 100644
--- a/crates/auth/src/server/routes.rs
+++ b/crates/auth/src/server/routes.rs
@@ -1,5 +1,8 @@
pub mod authorised;
pub mod discord;
+
+use std::fmt::Display;
+
use axum::response::IntoResponse;
use serde::Deserialize;
@@ -9,6 +12,18 @@ pub enum Provider {
Discord,
}
+impl Display for Provider {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Provider::Discord => "discord",
+ }
+ )
+ }
+}
+
pub async fn health_check() -> impl IntoResponse {
let name = env!("CARGO_PKG_NAME");
let ver = env!("CARGO_PKG_VERSION");
diff --git a/crates/auth/src/server/routes/authorised.rs b/crates/auth/src/server/routes/authorised.rs
index d493db5..27f02bc 100644
--- a/crates/auth/src/server/routes/authorised.rs
+++ b/crates/auth/src/server/routes/authorised.rs
@@ -8,13 +8,15 @@ use axum::{
};
use axum_extra::{TypedHeader, headers};
use oauth2::{AuthorizationCode, TokenResponse};
-use reqwest::header::SET_COOKIE;
+use reqwest::{StatusCode, header::SET_COOKIE};
use serde::{Deserialize, Serialize};
use sqlx::types::uuid;
+use time::OffsetDateTime;
use tower_sessions::{
SessionStore,
session::{Id, Record},
};
+use uuid::Uuid;
use crate::{
error::AppError,
@@ -37,17 +39,38 @@ struct User {
avatar: Option<String>,
username: String,
discriminator: String,
+ verified: bool,
+ email: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+struct DbUser {
+ id: Uuid,
+ email: String,
+ created_at: OffsetDateTime,
+ updated_at: OffsetDateTime,
}
/// The cookie to store the session id for user information.
const SESSION_COOKIE: &str = "info";
const SESSION_DATA_KEY: &str = "data";
+#[derive(Debug, Serialize, Deserialize)]
+struct Claims {
+ iss: String,
+ sub: Uuid,
+ exp: i64,
+ iat: i64,
+ sid: String,
+ aud: String,
+}
+
pub async fn login_authorised(
Query(query): Query<AuthRequest>,
State(state): State<AppHandle>,
TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> Result<impl IntoResponse, AppError> {
+ let provider = query.provider.to_string();
let oauth_session_id = Id::from_str(
cookies
.get(OAUTH_CSRF_COOKIE)
@@ -66,19 +89,112 @@ pub async fn login_authorised(
.await
.context("failed in sending request request to authorisation server")?;
- let user_data: User = client
+ let user_data = client
// https://discord.com/developers/docs/resources/user#get-current-user
.get("https://discordapp.com/api/users/@me")
.bearer_auth(token.access_token().secret())
.send()
.await
.context("failed in sending request to target Url")?
- .json::<User>()
+ .json::<serde_json::Value>()
.await
.context("failed to deserialise response as JSON")?;
+ let user_data: User = serde_json::from_value(user_data)?;
+
+ if !user_data.verified {
+ return Ok((StatusCode::UNAUTHORIZED, "email is not verified").into_response());
+ }
+
// Create a new session filled with user data
let session_id = Id(i128::from_le_bytes(uuid::Uuid::new_v4().to_bytes_le()));
+
+ let mut transaction = state.services.postgres.begin().await?;
+
+ let user = sqlx::query_as!(
+ DbUser,
+ "
+ select
+ p.*
+ from
+ auth_user p
+ inner join
+ oauth_account a
+ on
+ p.id=a.user_id
+ where a.provider_id = $1 and a.provider_user_id = $2
+ ",
+ provider,
+ user_data.id
+ )
+ .fetch_optional(&mut *transaction)
+ .await?;
+
+ let user = if let Some(user) = user {
+ println!("some");
+ user
+ } else {
+ println!("none");
+ let uuid = uuid::Uuid::now_v7();
+ let user = sqlx::query_as!(
+ DbUser,
+ "insert into auth_user (id, email) values ($1, $2)
+ on conflict (email) do update
+ set email = excluded.email
+ returning *;
+ ",
+ uuid,
+ user_data.email,
+ // user_data.avatar.as_ref().map(|value| {
+ // format!(
+ // "https://cdn.discordapp.com/avatars/{}/{value}",
+ // user_data.id
+ // )
+ // })
+ )
+ .fetch_one(&mut *transaction)
+ .await?;
+
+ sqlx::query_as!(
+ DbUser,
+ "with upsert as (
+ insert into oauth_account (provider_id, provider_user_id, user_id) values ($1, $2, $3)
+ on conflict (provider_id, provider_user_id) do update
+ set provider_id = excluded.provider_id -- no-op
+ returning user_id
+ )
+ select u.*
+ from upsert
+ join auth_user u on u.id = upsert.user_id;
+ ",
+ provider,
+ user_data.id,
+ user.id
+ )
+ .fetch_one(&mut *transaction)
+ .await?
+ };
+
+ let exp = OffsetDateTime::now_utc() + Duration::from_secs(15 * 60);
+
+ let claims = Claims {
+ sub: user.id,
+ // Mandatory expiry time as UTC timestamp
+ exp: exp.unix_timestamp(),
+ iss: "sellershut".to_owned(),
+ sid: session_id.to_string(),
+ aud: "sellershut".to_owned(),
+ iat: OffsetDateTime::now_utc().unix_timestamp(),
+ };
+
+ let token = jsonwebtoken::encode(
+ &jsonwebtoken::Header::default(),
+ &claims,
+ &jsonwebtoken::EncodingKey::from_secret(
+ state.local_config.oauth.jwt_encoding_key.as_bytes(),
+ ),
+ )?;
+
store
.create(&mut Record {
id: session_id,
@@ -93,15 +209,24 @@ pub async fn login_authorised(
.await
.context("failed in inserting serialised value into session")?;
- // Store session and get corresponding cookie.
+ sqlx::query!(
+ "insert into token (user_id, token, session_id) values ($1, $2, $3)",
+ user.id,
+ token,
+ session_id.to_string()
+ )
+ .execute(&mut *transaction)
+ .await?;
+
let cookie = format!("{SESSION_COOKIE}={session_id}; SameSite=Lax; HttpOnly; Secure; Path=/");
- // Set cookie
let mut headers = HeaderMap::new();
headers.insert(
SET_COOKIE,
cookie.parse().context("failed to parse cookie")?,
);
- Ok((headers, Redirect::to("/")))
+ transaction.commit().await?;
+
+ Ok((headers, Redirect::to("/")).into_response())
}