mirror of
https://github.com/Noratrieb/oh-oh.git
synced 2026-01-14 09:05:01 +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 (
|
||||
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,
|
||||
created INTEGER NOT NULL,
|
||||
user_agent TEXT NOT NULL,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX sessions_session_public_id ON sessions(session_public_id);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use askama::Template;
|
|||
use axum::{
|
||||
Form, Router,
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
routing::{get, post},
|
||||
};
|
||||
|
|
@ -60,6 +60,8 @@ async fn main() -> Result<()> {
|
|||
.route("/signup", get(signup).post(signup_post))
|
||||
.route("/login", get(login).post(login_post))
|
||||
.route("/2fa", get(list_2fa))
|
||||
.route("/sessions", get(list_sessions))
|
||||
.route("/sessions/delete", post(delete_session))
|
||||
.route("/2fa/delete", post(delete_2fa))
|
||||
.route("/add-totp", get(add_totp).post(add_totp_post))
|
||||
.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()))
|
||||
}
|
||||
|
||||
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)]
|
||||
struct Delete2faForm {
|
||||
device_id: i64,
|
||||
|
|
@ -253,11 +303,17 @@ struct UsernamePasswordForm {
|
|||
password: String,
|
||||
}
|
||||
|
||||
async fn make_session_cookie_for_user(db: &Db, user_id: i64) -> Result<Cookie<'static>, Response> {
|
||||
let session = session::create_session(&db, user_id).await.map_err(|err| {
|
||||
error!(?err, "Failed to create session for user");
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
})?;
|
||||
async fn make_session_cookie_for_user(
|
||||
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");
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
})?;
|
||||
|
||||
Ok(Cookie::build((SESSION_ID_COOKIE_NAME, session.0))
|
||||
.secure(true)
|
||||
|
|
@ -272,6 +328,7 @@ async fn make_session_cookie_for_user(db: &Db, user_id: i64) -> Result<Cookie<'s
|
|||
}
|
||||
|
||||
async fn signup_post(
|
||||
headers: HeaderMap,
|
||||
State(db): State<Db>,
|
||||
jar: CookieJar,
|
||||
Form(signup): Form<UsernamePasswordForm>,
|
||||
|
|
@ -294,12 +351,18 @@ async fn signup_post(
|
|||
.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("/")))
|
||||
}
|
||||
|
||||
async fn login_post(
|
||||
headers: HeaderMap,
|
||||
State(db): State<Db>,
|
||||
jar: CookieJar,
|
||||
Form(login): Form<UsernamePasswordForm>,
|
||||
|
|
@ -315,7 +378,12 @@ async fn login_post(
|
|||
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("/")))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,18 +34,48 @@ 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];
|
||||
rand_core::OsRng.fill_bytes(&mut session_id);
|
||||
let session_id = format!("idpsess_{}", hex::encode(session_id));
|
||||
|
||||
sqlx::query("insert into sessions (session_id, user_id, created) values (?, ?, ?)")
|
||||
.bind(&session_id)
|
||||
.bind(user_id)
|
||||
.bind(jiff::Timestamp::now().as_millisecond())
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.wrap_err("inserting new session")?;
|
||||
sqlx::query(
|
||||
"insert into sessions (session_id, user_id, created, user_agent) values (?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&session_id)
|
||||
.bind(user_id)
|
||||
.bind(jiff::Timestamp::now().as_millisecond())
|
||||
.bind(user_agent)
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.wrap_err("inserting new session")?;
|
||||
|
||||
Ok(SessionId(session_id))
|
||||
}
|
||||
|
|
@ -63,7 +93,7 @@ impl FromRequestParts<Db> for UserSession {
|
|||
let jar = CookieJar::from_headers(&parts.headers);
|
||||
|
||||
match jar.get(crate::SESSION_ID_COOKIE_NAME) {
|
||||
None => Err(StatusCode::FORBIDDEN.into_response()),
|
||||
None => Ok(UserSession(None)),
|
||||
Some(cookie) => {
|
||||
let sess = find_session(&db, cookie.value()).await;
|
||||
match sess {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@
|
|||
<a href="/users">List all users</a>
|
||||
</div>
|
||||
{% if let Some(username) = username %}
|
||||
<div>
|
||||
<a href="/sessions">List all active sessions your account</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/add-totp">Add a new 2FA device to your account</a>
|
||||
</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