Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.

Commit bf9eb53

Browse files
committed
Add log-out, fix user auto-log-out
1 parent 97cbf34 commit bf9eb53

9 files changed

Lines changed: 89 additions & 43 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "qxhttpd"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
edition = "2024"
55

66
[dependencies]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DELETE FROM changes;
2+
ALTER TABLE changes ADD COLUMN status_message TEXT;

src/auth.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ use rocket::log::private::info;
77
use rocket::response::{Debug, Redirect};
88
use rocket_oauth2::{OAuth2, TokenResponse};
99
use serde_json::Value;
10-
use crate::{QxSession, QxSessionId, SharedQxState};
10+
use crate::{MaybeSessionId, QxSession, QxSessionId, SharedQxState};
1111

12-
#[derive(Clone, serde::Serialize)]
12+
#[derive(serde::Serialize, Clone, Debug)]
1313
pub struct UserInfo {
1414
name: String,
1515
pub(crate) email: String,
@@ -62,6 +62,14 @@ struct GoogleUserInfo {
6262
email: Value,
6363
picture: Value,
6464
}
65+
#[get("/logout")]
66+
async fn logout(session_id: MaybeSessionId, state: &State<SharedQxState>) -> Redirect {
67+
if let Some(session_id) = session_id.0 {
68+
state.write().await.sessions.remove(&session_id);
69+
}
70+
Redirect::to("/")
71+
}
72+
6573
#[get("/login")]
6674
async fn login(state: &State<SharedQxState>) -> Redirect {
6775
// must be the same host as redirect_uri, both have to be localhost or 127.0.0.1
@@ -107,6 +115,9 @@ async fn google_auth(token: TokenResponse<GoogleUserInfo>, cookies: &CookieJar<'
107115
}
108116
let session_id = generate_session_id();
109117
info!("User log in, name: {}, email: {}, picture: {}", user_info.name, user_info.email, user_info.picture);
118+
119+
info!("insert session_id: {session_id:?}");
120+
110121
state.write().await.sessions.insert(QxSessionId(session_id.clone()), QxSession{ user_info });
111122
// Set a private cookie with the user's name, and redirect to the home page.
112123
cookies.add_private(
@@ -120,6 +131,7 @@ async fn google_auth(token: TokenResponse<GoogleUserInfo>, cookies: &CookieJar<'
120131

121132
pub fn extend(rocket: Rocket<Build>) -> Rocket<Build> {
122133
rocket.mount("/", routes![
134+
logout,
123135
login,
124136
google_login,
125137
google_auth,

src/changes.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ use crate::qxdatetime::QxDateTime;
1515
use sqlx::{Encode, Sqlite};
1616
use sqlx::query::{Query};
1717
use sqlx::sqlite::{SqliteArgumentValue, SqliteArguments};
18-
use log::info;
1918
use crate::db::{get_event_db, DbPool};
2019
use crate::oc::OCheckListChange;
2120
use crate::util::{anyhow_to_custom_error, sqlx_to_anyhow, sqlx_to_custom_error};
@@ -274,7 +273,7 @@ async fn get_changes(event_id: EventId, from_id: Option<i64>, state: &State<Shar
274273

275274
#[post("/api/event/<event_id>/changes/run-update-request", data = "<change>")]
276275
pub async fn add_run_update_request_change(event_id: EventId, session_id: QxSessionId, change: Json<QxRunChange>, state: &State<SharedQxState>) -> Result<(), Custom<String>> {
277-
let user = user_info(session_id, state).await?;
276+
let user = user_info(&session_id, state).await?;
278277
let change = change.into_inner();
279278
let data_type = DataType::RunUpdateRequest;
280279
let data = ChangeData::RunUpdateRequest(change.clone());
@@ -402,7 +401,7 @@ async fn api_get_changes(event_id: EventId, from_id: Option<i64>, data_type: Opt
402401

403402
let query = query_builder.build_query_as::<ChangesRecord>();
404403
let records: Vec<_> = query.fetch_all(&edb).await.map_err(sqlx_to_custom_error)?;
405-
info!("records: {:?}", records);
404+
// info!("records: {:?}", records);
406405
Ok(records.into())
407406
}
408407

src/event.rs

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use std::fs::OpenOptions;
22
use rocket::serde::json::Json;
33
use std::io::{Cursor, Read};
44
use anyhow::anyhow;
5-
use base64::engine::general_purpose;
65
use image::ImageFormat;
76
use rocket::form::{Contextual, Form};
87
use rocket::http::{ContentType, Status};
@@ -121,7 +120,7 @@ struct EventFormValues<'v> {
121120
// need, do not use `Contextual`. Use the equivalent of `Form<Submit<'_>>`.
122121
#[post("/event", data = "<form>")]
123122
async fn post_event<'r>(form: Form<Contextual<'r, EventFormValues<'r>>>, session_id: QxSessionId, state: &State<SharedQxState>, db: &State<DbPool>) -> Result<Redirect, Custom<String>> {
124-
let user = user_info(session_id, state).await?;
123+
let user = user_info(&session_id, state).await?;
125124
let vals = form.value.as_ref().ok_or(Custom(Status::BadRequest, "Form data invalid".to_string()))?;
126125
let start_time = QxDateTime::parse_from_iso(vals.start_time)
127126
.map_err(|e| Custom(Status::BadRequest, format!("Unrecognized date-time string: {}, error: {e}", vals.start_time)))?;
@@ -149,32 +148,51 @@ async fn post_event<'r>(form: Form<Contextual<'r, EventFormValues<'r>>>, session
149148
let event_id = save_event(&event, db).await.map_err(|e| Custom(Status::BadRequest, e.to_string()))?;
150149
Ok(Redirect::to(format!("/event/{event_id}")))
151150
}
152-
pub async fn user_info(session_id: QxSessionId, state: &State<SharedQxState>) -> Result<UserInfo, Custom<String>> {
151+
pub async fn user_info(session_id: &QxSessionId, state: &State<SharedQxState>) -> Result<UserInfo, Custom<String>> {
153152
state.read().await
154153
.sessions.get(&session_id).map(|s| s.user_info.clone()).ok_or( Custom(Status::Unauthorized, "Invalid session ID".to_string()) )
155154
}
156-
pub async fn user_info_opt(session_id: MaybeSessionId, state: &State<SharedQxState>) -> anyhow::Result<Option<UserInfo>> {
157-
match session_id {
158-
MaybeSessionId::None => Ok(None),
159-
MaybeSessionId::Some(session_id) => {
160-
let user_info = state.read().await
161-
.sessions.get(&session_id).map(|s| s.user_info.clone());
162-
Ok(user_info)
155+
pub async fn user_info_opt(session_id: Option<&QxSessionId>, state: &State<SharedQxState>) -> anyhow::Result<Option<UserInfo>> {
156+
match &session_id {
157+
None => Ok(None),
158+
Some(session_id) => {
159+
get_user_info(session_id, state).await
163160
}
164161
}
165162
}
166163

167-
pub async fn user_and_event_owner_opt(event_id: EventId, session_id: MaybeSessionId, state: &State<SharedQxState>, gdb: &State<DbPool>) -> anyhow::Result<Option<UserInfo>> {
164+
pub async fn get_user_info(session_id: &QxSessionId, state: &State<SharedQxState>) -> anyhow::Result<Option<UserInfo>> {
165+
let user_info = state.read().await
166+
.sessions.get(session_id).map(|s| s.user_info.clone());
167+
Ok(user_info)
168+
}
169+
170+
pub async fn event_owner_opt(event_id: EventId, session_id: MaybeSessionId, state: &State<SharedQxState>, gdb: &State<DbPool>) -> anyhow::Result<Option<UserInfo>> {
168171
let event = load_event(event_id, gdb).await?;
169-
let user = user_info_opt(session_id, state).await?
172+
let user = user_info_opt(session_id.0.as_ref(), state).await?
170173
.and_then(|user| if user.email == event.owner {Some(user)} else {None});
171174
Ok(user)
172175
}
173176

174-
async fn event_edit_insert(event_id: Option<EventId>, session_id: QxSessionId, state: &State<SharedQxState>, db: &State<DbPool>) -> Result<Template, Custom<String>> {
175-
let user = user_info(session_id, state).await?;
177+
pub fn is_event_owner(event: &EventRecord, user: Option<&UserInfo>) -> bool {
178+
if let Some(user) = user {
179+
user.email == event.owner
180+
} else {
181+
false
182+
}
183+
}
184+
185+
async fn event_edit_insert(event_id: Option<EventId>, session_id: &QxSessionId, state: &State<SharedQxState>, db: &State<DbPool>) -> Result<Template, Custom<String>> {
186+
let user = get_user_info(session_id, state).await
187+
.and_then(|u| if let Some(u) = u {Ok(u)} else {Err(anyhow!("Invalid session ID"))})
188+
.map_err(anyhow_to_custom_error)?;
176189
let event = if let Some(event_id) = event_id {
177-
load_event_info(event_id, db).await?
190+
let event = load_event_info(event_id, db).await?;
191+
if is_event_owner(&event, Some(&user)) {
192+
event
193+
} else {
194+
return Err(Custom(Status::Unauthorized, "Event owner mismatch".to_string()))
195+
}
178196
} else {
179197
EventRecord::new(&user.email)
180198
};
@@ -186,7 +204,7 @@ async fn event_edit_insert(event_id: Option<EventId>, session_id: QxSessionId, s
186204
let mut cursor = Cursor::new(&mut buffer);
187205
image.write_to(&mut cursor, ImageFormat::Png).unwrap();
188206
// Encode the image buffer to base64
189-
general_purpose::STANDARD.encode(&buffer)
207+
base64::engine::general_purpose::STANDARD.encode(&buffer)
190208
};
191209
Ok(Template::render("event-edit", context! {
192210
event_id,
@@ -205,15 +223,15 @@ async fn event_drop(event_id: EventId, db: &State<DbPool>) -> Result<(), anyhow:
205223
}
206224
#[get("/event/create")]
207225
async fn event_create(session_id: QxSessionId, state: &State<SharedQxState>, db: &State<DbPool>) -> Result<Template, Custom<String>> {
208-
event_edit_insert(None, session_id, state, db).await
226+
event_edit_insert(None, &session_id, state, db).await
209227
}
210228
#[get("/event/<event_id>/edit")]
211229
async fn event_edit(event_id: EventId, session_id: QxSessionId, state: &State<SharedQxState>, db: &State<DbPool>) -> Result<Template, Custom<String>> {
212-
event_edit_insert(Some(event_id), session_id, state, db).await
230+
event_edit_insert(Some(event_id), &session_id, state, db).await
213231
}
214232
#[get("/event/<event_id>/delete")]
215233
async fn event_delete(event_id: EventId, session_id: QxSessionId, state: &State<SharedQxState>, db: &State<DbPool>) -> Result<Redirect, Custom<String>> {
216-
let user = user_info(session_id, state).await?;
234+
let user = user_info(&session_id, state).await?;
217235
let event = load_event_info(event_id, db).await?;
218236
if event.owner == user.email {
219237
event_drop(event_id, db).await.map_err(|e| Custom(Status::InternalServerError, e.to_string()))?;
@@ -226,10 +244,12 @@ async fn event_delete(event_id: EventId, session_id: QxSessionId, state: &State<
226244
#[get("/event/<event_id>")]
227245
async fn get_event(event_id: EventId, session_id: MaybeSessionId, state: &State<SharedQxState>, gdb: &State<DbPool>) -> Result<Template, Custom<String>> {
228246
let event = load_event_info(event_id, gdb).await?;
229-
let user = user_and_event_owner_opt(event_id, session_id, state, gdb).await.map_err(anyhow_to_custom_error)?;
247+
let user = user_info_opt(session_id.0.as_ref(), state).await.map_err(anyhow_to_custom_error)?;
248+
let is_event_owner = is_event_owner(&event, user.as_ref());
230249
let files = files::list_files(event_id, state).await?;
231250
Ok(Template::render("event", context! {
232251
user,
252+
is_event_owner,
233253
event,
234254
files,
235255
}))
@@ -262,8 +282,10 @@ async fn post_api_event_current(api_token: QxApiToken, posted_event: Json<Posted
262282

263283
#[get("/event/<event_id>/startlist?<class_name>")]
264284
async fn get_event_start_list(event_id: EventId, session_id: MaybeSessionId, class_name: Option<&str>, state: &State<SharedQxState>, gdb: &State<DbPool>) -> Result<Template, Custom<String>> {
285+
info!("GET session_id: {session_id:?}");
265286
let event = load_event_info(event_id, gdb).await?;
266-
let user = user_and_event_owner_opt(event_id, session_id, state, gdb).await.map_err(anyhow_to_custom_error)?;
287+
let user = user_info_opt(session_id.0.as_ref(), state).await.map_err(anyhow_to_custom_error)?;
288+
info!("GET user: {user:?}");
267289
let edb = get_event_db(event_id, state).await.map_err(anyhow_to_custom_error)?;
268290
let classes = sqlx::query_as::<_, ClassesRecord>("SELECT * FROM classes ORDER BY name")
269291
.fetch_all(&edb).await.map_err(sqlx_to_custom_error)?;
@@ -350,7 +372,7 @@ async fn upload_start_list(qx_api_token: QxApiToken, data: Data<'_>, content_typ
350372
}
351373
#[post("/api/event/<event_id>/upload/startlist", data = "<data>")]
352374
async fn upload_start_list_user(event_id: EventId, data: Data<'_>, content_type: &ContentType, session_id: MaybeSessionId, state: &State<SharedQxState>, gdb: &State<DbPool>) -> Result<String, Custom<String>> {
353-
let Some(_user) = user_and_event_owner_opt(event_id, session_id, state, gdb).await.map_err(anyhow_to_custom_error)? else {
375+
let Some(_event_owner) = event_owner_opt(event_id, session_id, state, gdb).await.map_err(anyhow_to_custom_error)? else {
354376
return Err(Custom(Status::Unauthorized, String::from("Session expired or not valid")));
355377
};
356378
let event_info = load_event_info(event_id, gdb).await?;

src/main.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ impl AppConfig {
4848
self.server_address == "127.0.0.1"
4949
}
5050
}
51+
#[derive(Clone, Debug)]
5152
struct QxSession {
5253
user_info: UserInfo,
5354
}
54-
#[derive(Eq, Hash, PartialEq)]
55+
#[derive(Eq, Hash, PartialEq, Clone, Debug)]
5556
struct QxSessionId(String);
5657
#[rocket::async_trait]
5758
impl<'r> request::FromRequest<'r> for QxSessionId {
@@ -68,10 +69,9 @@ impl<'r> request::FromRequest<'r> for QxSessionId {
6869
}
6970
}
7071

71-
enum MaybeSessionId {
72-
None,
73-
Some(QxSessionId),
74-
}
72+
#[derive(Debug)]
73+
pub struct MaybeSessionId(Option<QxSessionId>);
74+
7575
#[rocket::async_trait]
7676
impl<'r> request::FromRequest<'r> for MaybeSessionId {
7777
type Error = ();
@@ -81,9 +81,9 @@ impl<'r> request::FromRequest<'r> for MaybeSessionId {
8181
.await
8282
.expect("request cookies");
8383
if let Some(cookie) = cookies.get_private(QX_SESSION_ID) {
84-
return request::Outcome::Success(Self::Some(QxSessionId(cookie.value().to_string())));
84+
return request::Outcome::Success(Self(Some(QxSessionId(cookie.value().to_string()))));
8585
}
86-
request::Outcome::Success(Self::None)
86+
request::Outcome::Success(Self(None))
8787
}
8888
}
8989

@@ -156,7 +156,7 @@ type SharedQxState = tokio::sync::RwLock<QxState>;
156156

157157
#[get("/")]
158158
async fn index(sid: MaybeSessionId, state: &State<SharedQxState>, db: &State<DbPool>) -> Result<Template, Custom<String>> {
159-
let user = user_info_opt(sid, state).await.map_err(anyhow_to_custom_error)?;
159+
let user = user_info_opt(sid.0.as_ref(), state).await.map_err(anyhow_to_custom_error)?;
160160
let pool = &db.0;
161161
let events: Vec<EventRecord> = sqlx::query_as("SELECT * FROM events")
162162
.fetch_all(pool)

static/js/utils.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ function formatRunTable(changes) {
5656
else if (col_name === "time") {
5757
cell_val = obtime(msecSinceUntil(row.dataset.start_time, row.dataset.finish_time));
5858
}
59-
else if (col_name === "name") {
60-
cell_val = `${row.dataset.last_name} ${row.dataset.first_name}`;
61-
}
6259
else {
6360
const val = row.dataset[col_name];
6461
// console.log(i, j, col_name, val);

templates/event.html.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<h2>{{ event.name }}</h2>
44
<div class="w3-bar">
5-
{{#if user}}
5+
{{#if is_event_owner}}
66
<a href="/event/{{event.id}}/edit" class="w3-button w3-theme w3-round-large w3-border"><i class="fa fa-cog"></i> edit</a>
77
<button onclick="document.getElementById('uploadStartListDialog').style.display='block'" class="w3-button w3-theme w3-round-large w3-border">Upload start list</button>
88
{{/if}}

templates/nav.html.hbs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
<a href="/" class="w3-bar-item w3-button">Quick Exchange</a>
66
<!-- <a href="#" class="w3-bar-item w3-button w3-hide-small w3-hover-white">About</a>-->
77
{{#if user}}
8-
<span class="w3-bar-item w3-right">{{user.name}}</span>
9-
<img src="{{user.picture}}" alt="avatar" class="w3-circle w3-right" width="40" height="40">
8+
<div class="w3-dropdown-click w3-right">
9+
<img src="{{user.picture}}" alt="avatar" class="w3-circle" width="40" height="40">
10+
<span onclick="toggleDropDown()" class="w3-button">{{user.name}}</span>
11+
<div id="DropdownContent" class="w3-dropdown-content w3-bar-block w3-border">
12+
<a href="/logout" class="w3-bar-item w3-button">Log out</a>
13+
</div>
14+
</div>
1015
{{else}}
11-
<a class="w3-bar-item w3-button w3-hover-black w3-right" href="/login">Login</a>
16+
<a class="w3-bar-item w3-button w3-hover-black w3-right" href="/login">Log in</a>
1217
{{/if}}
1318
</div>
1419
<!--</div>-->
@@ -49,4 +54,13 @@
4954
mySidebar.style.display = "none";
5055
overlayBg.style.display = "none";
5156
}
57+
58+
function toggleDropDown() {
59+
var x = document.getElementById("DropdownContent");
60+
if (x.className.indexOf("w3-show") === -1) {
61+
x.className += " w3-show";
62+
} else {
63+
x.className = x.className.replace(" w3-show", "");
64+
}
65+
}
5266
</script>

0 commit comments

Comments
 (0)