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",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time",
|
||||
"time 0.1.45",
|
||||
"wasm-bindgen",
|
||||
"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]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.4"
|
||||
|
@ -696,6 +706,7 @@ dependencies = [
|
|||
"axum",
|
||||
"axum-macros",
|
||||
"chrono",
|
||||
"cookie",
|
||||
"figment",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -703,6 +714,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
@ -1402,6 +1414,33 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
|
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
axum = "0.6"
|
||||
axum-macros = "0.3"
|
||||
cookie = "0.17"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tokio = { version = "1.28", features = ["full"] }
|
||||
|
@ -17,6 +18,7 @@ figment = { version = "0.10", features = ["toml", "env"] }
|
|||
chrono = { version = "0.4", features = ["serde", "clock"] }
|
||||
anyhow = "1.0"
|
||||
argon2 = "0.5"
|
||||
url = "2.4"
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
32
src/auth.rs
32
src/auth.rs
|
@ -3,10 +3,7 @@ use argon2::{
|
|||
password_hash::{rand_core::OsRng, SaltString},
|
||||
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||
};
|
||||
use chrono::{
|
||||
serde::{ts_seconds, ts_seconds_option},
|
||||
DateTime, Duration, Utc,
|
||||
};
|
||||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Pool, Postgres};
|
||||
use uuid::Uuid;
|
||||
|
@ -35,12 +32,9 @@ pub struct User {
|
|||
pub roles: Vec<Uuid>,
|
||||
|
||||
/// Times
|
||||
#[serde(with = "ts_seconds")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[serde(with = "ts_seconds_option")]
|
||||
pub modified_at: Option<DateTime<Utc>>,
|
||||
#[serde(with = "ts_seconds_option")]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub modified_at: Option<NaiveDateTime>,
|
||||
pub deleted_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
|
@ -79,7 +73,7 @@ impl User {
|
|||
id: result.id,
|
||||
name: username.to_owned(),
|
||||
roles: roles.to_owned(),
|
||||
created_at: result.created_at.and_utc(),
|
||||
created_at: result.created_at,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
@ -113,23 +107,21 @@ pub struct Session {
|
|||
pub secret: String,
|
||||
|
||||
/// Times
|
||||
#[serde(with = "ts_seconds")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[serde(with = "ts_seconds")]
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub expires_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
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 secret = random();
|
||||
let result = sqlx::query!(
|
||||
"INSERT INTO sessions (actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||
user_id,
|
||||
secret,
|
||||
now.naive_utc(),
|
||||
expires.naive_utc()
|
||||
now,
|
||||
expires
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
@ -150,13 +142,13 @@ impl Session {
|
|||
}
|
||||
|
||||
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();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE sessions SET secret = $1, expires_at = $2 WHERE id = $3 RETURNING id",
|
||||
secret,
|
||||
expires_at.naive_utc(),
|
||||
expires_at,
|
||||
self.id
|
||||
)
|
||||
.fetch_one(pool)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::error::Error;
|
||||
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
|
|
|
@ -41,6 +41,7 @@ async fn main() -> Result<()> {
|
|||
|
||||
let app = Router::new()
|
||||
.route("/auth/login", post(routes::auth::login))
|
||||
.route("/me", get(routes::auth::me))
|
||||
.route("/pages/:site/:slug", get(routes::content::page))
|
||||
.route("/admin/bootstrap", post(routes::admin::bootstrap))
|
||||
.with_state(shared_state);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::Json;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::auth::{random, User};
|
||||
|
@ -9,16 +10,17 @@ use crate::error::AppError;
|
|||
use crate::roles::ROLE_SUPERADMIN;
|
||||
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!
|
||||
let empty = sqlx::query!(
|
||||
"SELECT CASE WHEN EXISTS(SELECT 1 FROM users) THEN false ELSE true END AS empty;"
|
||||
)
|
||||
.map(|row| row.empty.unwrap_or(true))
|
||||
.fetch_one(&state.database)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
if empty {
|
||||
if !empty {
|
||||
return Err(AppError::ClientError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: "already-setup".into(),
|
||||
|
@ -35,7 +37,8 @@ pub async fn bootstrap(State(state): State<Arc<AppState>>) -> Result<Json<Value>
|
|||
&password,
|
||||
&[ROLE_SUPERADMIN].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
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 cookie::Cookie;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
|
@ -19,8 +25,10 @@ pub struct LoginRequest {
|
|||
pub async fn login(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let user = User::find(&state.database, payload.username.as_str()).await?;
|
||||
) -> impl IntoResponse {
|
||||
let user = User::find(&state.database, payload.username.as_str())
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let invalid = || -> AppError {
|
||||
AppError::ClientError {
|
||||
|
@ -33,7 +41,7 @@ pub async fn login(
|
|||
let user = user.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());
|
||||
}
|
||||
|
||||
|
@ -42,9 +50,28 @@ pub async fn login(
|
|||
user.id,
|
||||
Duration::seconds(state.config.session_duration.into()),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
Ok(Json(
|
||||
json!({ "session_token": session.token(), "expires_at": session.expires_at }),
|
||||
))
|
||||
let token = session.token();
|
||||
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 sqlx::{Pool, Postgres};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
pub bind: String,
|
||||
pub database_url: String,
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
bind: "127.0.0.1:3000".to_owned(),
|
||||
database_url: "postgres://artificiale:changeme@localhost/artificiale".to_owned(),
|
||||
bind: "127.0.0.1:3000".into(),
|
||||
database_url: "postgres://artificiale:changeme@localhost/artificiale".into(),
|
||||
session_duration: 1440, // 24min
|
||||
base_url: "http://localhost".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue