mirror of
https://github.com/Noratrieb/oh-oh.git
synced 2026-01-14 17:15:02 +01:00
stuff
This commit is contained in:
parent
c789f7ad15
commit
730cf9d176
5 changed files with 150 additions and 18 deletions
|
|
@ -1,7 +1,11 @@
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
session_id TEXT NOT NULL PRIMARY KEY,
|
session_public_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
session_id TEXT NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
created INTEGER NOT NULL,
|
created INTEGER NOT NULL,
|
||||||
|
user_agent TEXT NOT NULL,
|
||||||
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE INDEX sessions_session_public_id ON sessions(session_public_id);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, Router,
|
Form, Router,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::StatusCode,
|
http::{HeaderMap, StatusCode},
|
||||||
response::{Html, IntoResponse, Redirect, Response},
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|
@ -60,6 +60,8 @@ async fn main() -> Result<()> {
|
||||||
.route("/signup", get(signup).post(signup_post))
|
.route("/signup", get(signup).post(signup_post))
|
||||||
.route("/login", get(login).post(login_post))
|
.route("/login", get(login).post(login_post))
|
||||||
.route("/2fa", get(list_2fa))
|
.route("/2fa", get(list_2fa))
|
||||||
|
.route("/sessions", get(list_sessions))
|
||||||
|
.route("/sessions/delete", post(delete_session))
|
||||||
.route("/2fa/delete", post(delete_2fa))
|
.route("/2fa/delete", post(delete_2fa))
|
||||||
.route("/add-totp", get(add_totp).post(add_totp_post))
|
.route("/add-totp", get(add_totp).post(add_totp_post))
|
||||||
.route("/users", get(users))
|
.route("/users", get(users))
|
||||||
|
|
@ -153,6 +155,54 @@ async fn list_2fa(user: UserSession, State(db): State<Db>) -> Result<impl IntoRe
|
||||||
Ok(Html(Template { devices }.render().unwrap()))
|
Ok(Html(Template { devices }.render().unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_sessions(
|
||||||
|
user: UserSession,
|
||||||
|
State(db): State<Db>,
|
||||||
|
) -> Result<impl IntoResponse, Response> {
|
||||||
|
let Some(user) = user.0 else {
|
||||||
|
return Err(Redirect::to("/").into_response());
|
||||||
|
};
|
||||||
|
|
||||||
|
let sessions = session::find_sessions_for_user(&db, user.user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(?err, "Error fetching sessions");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
#[derive(askama::Template)]
|
||||||
|
#[template(path = "sessions.html")]
|
||||||
|
struct Template {
|
||||||
|
sessions: Vec<session::SessionForList>,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Html(Template { sessions }.render().unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DeleteSessionForm {
|
||||||
|
session_public_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_session(
|
||||||
|
user: UserSession,
|
||||||
|
State(db): State<Db>,
|
||||||
|
Form(form): Form<DeleteSessionForm>,
|
||||||
|
) -> Result<impl IntoResponse, Response> {
|
||||||
|
let Some(user) = user.0 else {
|
||||||
|
return Err(Redirect::to("/").into_response());
|
||||||
|
};
|
||||||
|
|
||||||
|
session::delete_session(&db, user.user_id, form.session_public_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(?err, "Failed to delete session");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Redirect::to("/sessions"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Delete2faForm {
|
struct Delete2faForm {
|
||||||
device_id: i64,
|
device_id: i64,
|
||||||
|
|
@ -253,8 +303,14 @@ struct UsernamePasswordForm {
|
||||||
password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn make_session_cookie_for_user(db: &Db, user_id: i64) -> Result<Cookie<'static>, Response> {
|
async fn make_session_cookie_for_user(
|
||||||
let session = session::create_session(&db, user_id).await.map_err(|err| {
|
db: &Db,
|
||||||
|
user_id: i64,
|
||||||
|
user_agent: &str,
|
||||||
|
) -> Result<Cookie<'static>, Response> {
|
||||||
|
let session = session::create_session(&db, user_id, user_agent)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
error!(?err, "Failed to create session for user");
|
error!(?err, "Failed to create session for user");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -272,6 +328,7 @@ async fn make_session_cookie_for_user(db: &Db, user_id: i64) -> Result<Cookie<'s
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn signup_post(
|
async fn signup_post(
|
||||||
|
headers: HeaderMap,
|
||||||
State(db): State<Db>,
|
State(db): State<Db>,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
Form(signup): Form<UsernamePasswordForm>,
|
Form(signup): Form<UsernamePasswordForm>,
|
||||||
|
|
@ -294,12 +351,18 @@ async fn signup_post(
|
||||||
.into_response());
|
.into_response());
|
||||||
};
|
};
|
||||||
|
|
||||||
let session_id = make_session_cookie_for_user(&db, user.id).await?;
|
let user_agent = headers
|
||||||
|
.get(axum::http::header::USER_AGENT)
|
||||||
|
.and_then(|v| str::from_utf8(v.as_bytes()).ok())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
|
let session_id = make_session_cookie_for_user(&db, user.id, user_agent).await?;
|
||||||
|
|
||||||
Ok((jar.add(session_id), Redirect::to("/")))
|
Ok((jar.add(session_id), Redirect::to("/")))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn login_post(
|
async fn login_post(
|
||||||
|
headers: HeaderMap,
|
||||||
State(db): State<Db>,
|
State(db): State<Db>,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
Form(login): Form<UsernamePasswordForm>,
|
Form(login): Form<UsernamePasswordForm>,
|
||||||
|
|
@ -315,7 +378,12 @@ async fn login_post(
|
||||||
return Err(Html(LoginTemplate { error: true }.render().unwrap()).into_response());
|
return Err(Html(LoginTemplate { error: true }.render().unwrap()).into_response());
|
||||||
};
|
};
|
||||||
|
|
||||||
let session_id = make_session_cookie_for_user(&db, user.id).await?;
|
let user_agent = headers
|
||||||
|
.get(axum::http::header::USER_AGENT)
|
||||||
|
.and_then(|v| str::from_utf8(v.as_bytes()).ok())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
|
let session_id = make_session_cookie_for_user(&db, user.id, user_agent).await?;
|
||||||
|
|
||||||
Ok((jar.add(session_id), Redirect::to("/")))
|
Ok((jar.add(session_id), Redirect::to("/")))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,45 @@ pub async fn find_session(db: &Db, session_id: &str) -> Result<Option<SessionWit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_session(db: &Db, user_id: i64) -> Result<SessionId> {
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct SessionForList {
|
||||||
|
pub created: i64,
|
||||||
|
pub session_public_id: i64,
|
||||||
|
pub user_agent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_sessions_for_user(db: &Db, user_id: i64) -> Result<Vec<SessionForList>> {
|
||||||
|
sqlx::query_as::<_, SessionForList>(
|
||||||
|
"select created, session_public_id, user_agent from sessions where user_id = ?",
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_all(&db.pool)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to fetch sessions for user")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_session(db: &Db, user_id: i64, session_public_id: i64) -> Result<()> {
|
||||||
|
sqlx::query("delete from sessions where user_id = ? and session_public_id = ?")
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(session_public_id)
|
||||||
|
.execute(&db.pool)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to delete session")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_session(db: &Db, user_id: i64, user_agent: &str) -> Result<SessionId> {
|
||||||
let mut session_id = [0_u8; 32];
|
let mut session_id = [0_u8; 32];
|
||||||
rand_core::OsRng.fill_bytes(&mut session_id);
|
rand_core::OsRng.fill_bytes(&mut session_id);
|
||||||
let session_id = format!("idpsess_{}", hex::encode(session_id));
|
let session_id = format!("idpsess_{}", hex::encode(session_id));
|
||||||
|
|
||||||
sqlx::query("insert into sessions (session_id, user_id, created) values (?, ?, ?)")
|
sqlx::query(
|
||||||
|
"insert into sessions (session_id, user_id, created, user_agent) values (?, ?, ?, ?)",
|
||||||
|
)
|
||||||
.bind(&session_id)
|
.bind(&session_id)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(jiff::Timestamp::now().as_millisecond())
|
.bind(jiff::Timestamp::now().as_millisecond())
|
||||||
|
.bind(user_agent)
|
||||||
.execute(&db.pool)
|
.execute(&db.pool)
|
||||||
.await
|
.await
|
||||||
.wrap_err("inserting new session")?;
|
.wrap_err("inserting new session")?;
|
||||||
|
|
@ -63,7 +93,7 @@ impl FromRequestParts<Db> for UserSession {
|
||||||
let jar = CookieJar::from_headers(&parts.headers);
|
let jar = CookieJar::from_headers(&parts.headers);
|
||||||
|
|
||||||
match jar.get(crate::SESSION_ID_COOKIE_NAME) {
|
match jar.get(crate::SESSION_ID_COOKIE_NAME) {
|
||||||
None => Err(StatusCode::FORBIDDEN.into_response()),
|
None => Ok(UserSession(None)),
|
||||||
Some(cookie) => {
|
Some(cookie) => {
|
||||||
let sess = find_session(&db, cookie.value()).await;
|
let sess = find_session(&db, cookie.value()).await;
|
||||||
match sess {
|
match sess {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@
|
||||||
<a href="/users">List all users</a>
|
<a href="/users">List all users</a>
|
||||||
</div>
|
</div>
|
||||||
{% if let Some(username) = username %}
|
{% if let Some(username) = username %}
|
||||||
|
<div>
|
||||||
|
<a href="/sessions">List all active sessions your account</a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="/add-totp">Add a new 2FA device to your account</a>
|
<a href="/add-totp">Add a new 2FA device to your account</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
27
idp/templates/sessions.html
Normal file
27
idp/templates/sessions.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Sessions - IDP</title>
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>See all sessions your account</h1>
|
||||||
|
<a href="/">home</a>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for session in sessions %}
|
||||||
|
<li>
|
||||||
|
{{session.user_agent}} (added
|
||||||
|
{{jiff::Timestamp::from_millisecond(*session.created).unwrap()
|
||||||
|
.strftime("%A, %d %B %Y at %I:%M %Q")}})
|
||||||
|
<form method="post" action="/sessions/delete" class="fake-form">
|
||||||
|
<input type="hidden" name="session_public_id" value="{{session.session_public_id}}" />
|
||||||
|
<button type="submit">delete</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue