cookie handling
This commit is contained in:
parent
50e85f7de9
commit
b3380648bb
8 changed files with 119 additions and 39 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -238,11 +238,21 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time 0.1.45",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
|
||||||
|
dependencies = [
|
||||||
|
"time 0.3.22",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
@ -696,6 +706,7 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"cookie",
|
||||||
"figment",
|
"figment",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -703,6 +714,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1402,6 +1414,33 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
|
||||||
|
dependencies = [
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6"
|
axum = "0.6"
|
||||||
axum-macros = "0.3"
|
axum-macros = "0.3"
|
||||||
|
cookie = "0.17"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
tokio = { version = "1.28", features = ["full"] }
|
tokio = { version = "1.28", features = ["full"] }
|
||||||
|
@ -17,6 +18,7 @@ figment = { version = "0.10", features = ["toml", "env"] }
|
||||||
chrono = { version = "0.4", features = ["serde", "clock"] }
|
chrono = { version = "0.4", features = ["serde", "clock"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
|
url = "2.4"
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
32
src/auth.rs
32
src/auth.rs
|
@ -3,10 +3,7 @@ use argon2::{
|
||||||
password_hash::{rand_core::OsRng, SaltString},
|
password_hash::{rand_core::OsRng, SaltString},
|
||||||
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||||
};
|
};
|
||||||
use chrono::{
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
serde::{ts_seconds, ts_seconds_option},
|
|
||||||
DateTime, Duration, Utc,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow, Pool, Postgres};
|
use sqlx::{FromRow, Pool, Postgres};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -35,12 +32,9 @@ pub struct User {
|
||||||
pub roles: Vec<Uuid>,
|
pub roles: Vec<Uuid>,
|
||||||
|
|
||||||
/// Times
|
/// Times
|
||||||
#[serde(with = "ts_seconds")]
|
pub created_at: NaiveDateTime,
|
||||||
pub created_at: DateTime<Utc>,
|
pub modified_at: Option<NaiveDateTime>,
|
||||||
#[serde(with = "ts_seconds_option")]
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
pub modified_at: Option<DateTime<Utc>>,
|
|
||||||
#[serde(with = "ts_seconds_option")]
|
|
||||||
pub deleted_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for User {
|
impl Default for User {
|
||||||
|
@ -79,7 +73,7 @@ impl User {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
name: username.to_owned(),
|
name: username.to_owned(),
|
||||||
roles: roles.to_owned(),
|
roles: roles.to_owned(),
|
||||||
created_at: result.created_at.and_utc(),
|
created_at: result.created_at,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -113,23 +107,21 @@ pub struct Session {
|
||||||
pub secret: String,
|
pub secret: String,
|
||||||
|
|
||||||
/// Times
|
/// Times
|
||||||
#[serde(with = "ts_seconds")]
|
pub created_at: NaiveDateTime,
|
||||||
pub created_at: DateTime<Utc>,
|
pub expires_at: NaiveDateTime,
|
||||||
#[serde(with = "ts_seconds")]
|
|
||||||
pub expires_at: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub async fn create(pool: &Pool<Postgres>, user_id: Uuid, duration: Duration) -> Result<Self> {
|
pub async fn create(pool: &Pool<Postgres>, user_id: Uuid, duration: Duration) -> Result<Self> {
|
||||||
let now = Utc::now();
|
let now = Utc::now().naive_utc();
|
||||||
let expires = now + duration;
|
let expires = now + duration;
|
||||||
let secret = random();
|
let secret = random();
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
"INSERT INTO sessions (actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4) RETURNING id",
|
"INSERT INTO sessions (actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||||
user_id,
|
user_id,
|
||||||
secret,
|
secret,
|
||||||
now.naive_utc(),
|
now,
|
||||||
expires.naive_utc()
|
expires
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -150,13 +142,13 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(self: Self, pool: &Pool<Postgres>, duration: Duration) -> Result<Self> {
|
pub async fn refresh(self: Self, pool: &Pool<Postgres>, duration: Duration) -> Result<Self> {
|
||||||
let expires_at = Utc::now() + duration;
|
let expires_at = (Utc::now() + duration).naive_utc();
|
||||||
let secret = random();
|
let secret = random();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE sessions SET secret = $1, expires_at = $2 WHERE id = $3 RETURNING id",
|
"UPDATE sessions SET secret = $1, expires_at = $2 WHERE id = $3 RETURNING id",
|
||||||
secret,
|
secret,
|
||||||
expires_at.naive_utc(),
|
expires_at,
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
|
|
|
@ -41,6 +41,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/auth/login", post(routes::auth::login))
|
.route("/auth/login", post(routes::auth::login))
|
||||||
|
.route("/me", get(routes::auth::me))
|
||||||
.route("/pages/:site/:slug", get(routes::content::page))
|
.route("/pages/:site/:slug", get(routes::content::page))
|
||||||
.route("/admin/bootstrap", post(routes::admin::bootstrap))
|
.route("/admin/bootstrap", post(routes::admin::bootstrap))
|
||||||
.with_state(shared_state);
|
.with_state(shared_state);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use serde_json::{json, Value};
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::auth::{random, User};
|
use crate::auth::{random, User};
|
||||||
|
@ -9,16 +10,17 @@ use crate::error::AppError;
|
||||||
use crate::roles::ROLE_SUPERADMIN;
|
use crate::roles::ROLE_SUPERADMIN;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
pub async fn bootstrap(State(state): State<Arc<AppState>>) -> Result<Json<Value>, AppError> {
|
pub async fn bootstrap(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
// Only allow this request if the user table is completely empty!
|
// Only allow this request if the user table is completely empty!
|
||||||
let empty = sqlx::query!(
|
let empty = sqlx::query!(
|
||||||
"SELECT CASE WHEN EXISTS(SELECT 1 FROM users) THEN false ELSE true END AS empty;"
|
"SELECT CASE WHEN EXISTS(SELECT 1 FROM users) THEN false ELSE true END AS empty;"
|
||||||
)
|
)
|
||||||
.map(|row| row.empty.unwrap_or(true))
|
.map(|row| row.empty.unwrap_or(true))
|
||||||
.fetch_one(&state.database)
|
.fetch_one(&state.database)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
if empty {
|
if !empty {
|
||||||
return Err(AppError::ClientError {
|
return Err(AppError::ClientError {
|
||||||
status: StatusCode::BAD_REQUEST,
|
status: StatusCode::BAD_REQUEST,
|
||||||
code: "already-setup".into(),
|
code: "already-setup".into(),
|
||||||
|
@ -35,7 +37,8 @@ pub async fn bootstrap(State(state): State<Arc<AppState>>) -> Result<Json<Value>
|
||||||
&password,
|
&password,
|
||||||
&[ROLE_SUPERADMIN].to_vec(),
|
&[ROLE_SUPERADMIN].to_vec(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
Ok(Json(json!({"username": username, "password": password})))
|
Ok(Json(json!({"username": username, "password": password})))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
use axum::{extract::State, http::StatusCode, Json};
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::{header::SET_COOKIE, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use cookie::Cookie;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -19,8 +25,10 @@ pub struct LoginRequest {
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(payload): Json<LoginRequest>,
|
Json(payload): Json<LoginRequest>,
|
||||||
) -> Result<Json<Value>, AppError> {
|
) -> impl IntoResponse {
|
||||||
let user = User::find(&state.database, payload.username.as_str()).await?;
|
let user = User::find(&state.database, payload.username.as_str())
|
||||||
|
.await
|
||||||
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
let invalid = || -> AppError {
|
let invalid = || -> AppError {
|
||||||
AppError::ClientError {
|
AppError::ClientError {
|
||||||
|
@ -33,7 +41,7 @@ pub async fn login(
|
||||||
let user = user.ok_or_else(invalid)?;
|
let user = user.ok_or_else(invalid)?;
|
||||||
let plaintext = user.password.ok_or_else(invalid)?;
|
let plaintext = user.password.ok_or_else(invalid)?;
|
||||||
|
|
||||||
if !verify(&payload.password, &plaintext)? {
|
if !verify(&payload.password, &plaintext).map_err(AppError::from)? {
|
||||||
return Err(invalid());
|
return Err(invalid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,9 +50,28 @@ pub async fn login(
|
||||||
user.id,
|
user.id,
|
||||||
Duration::seconds(state.config.session_duration.into()),
|
Duration::seconds(state.config.session_duration.into()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
Ok(Json(
|
let token = session.token();
|
||||||
json!({ "session_token": session.token(), "expires_at": session.expires_at }),
|
let mut response: Response =
|
||||||
))
|
Json(json!({ "session_token": token, "expires_at": session.expires_at })).into_response();
|
||||||
|
|
||||||
|
let secure = state.config.secure();
|
||||||
|
let cookie = Cookie::build("session", token)
|
||||||
|
.domain(state.config.domain())
|
||||||
|
.secure(secure)
|
||||||
|
.http_only(!secure)
|
||||||
|
.path("/")
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(SET_COOKIE, cookie.to_string().parse()?);
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn me() -> String {
|
||||||
|
"test".into()
|
||||||
}
|
}
|
||||||
|
|
22
src/state.rs
22
src/state.rs
|
@ -1,19 +1,37 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub bind: String,
|
pub bind: String,
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub session_duration: i32, // in seconds
|
pub session_duration: i32, // in seconds
|
||||||
|
pub base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn secure(&self) -> bool {
|
||||||
|
Url::parse(&self.base_url)
|
||||||
|
.map(|x| x.scheme().to_owned())
|
||||||
|
.unwrap_or("http".to_owned())
|
||||||
|
== "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn domain(&self) -> String {
|
||||||
|
Url::parse(&self.base_url)
|
||||||
|
.map(|x| x.domain().unwrap_or("localhost").to_owned())
|
||||||
|
.unwrap_or("localhost".to_owned())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Config {
|
Config {
|
||||||
bind: "127.0.0.1:3000".to_owned(),
|
bind: "127.0.0.1:3000".into(),
|
||||||
database_url: "postgres://artificiale:changeme@localhost/artificiale".to_owned(),
|
database_url: "postgres://artificiale:changeme@localhost/artificiale".into(),
|
||||||
session_duration: 1440, // 24min
|
session_duration: 1440, // 24min
|
||||||
|
base_url: "http://localhost".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue