Skip to content

Commit 84702db

Browse files
committed
fix: give global admins more perms
1 parent f3bb7ed commit 84702db

File tree

7 files changed

+198
-71
lines changed

7 files changed

+198
-71
lines changed

Dioxus.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ android_manifest = "./mobile/AndroidManifest.xml"
55

66
[web.app]
77
title = "Terrier"
8+
index = "./index.html"
89

910
[web.app.head]
1011
html = """
11-
<link rel="manifest" href="/manifest.json">
12+
<link rel="manifest" href="/api/manifest.json">
1213
<meta name="theme-color" content="#ffffff">
1314
<meta name="apple-mobile-web-app-capable" content="yes">
1415
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

index.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Terrier</title>
5+
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta charset="UTF-8" />
8+
<link rel="manifest" href="/api/manifest.json">
9+
<meta name="theme-color" content="#ffffff">
10+
<meta name="apple-mobile-web-app-capable" content="yes">
11+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
12+
<link rel="apple-touch-icon" href="/icons/icon-192.png">
13+
</head>
14+
<body>
15+
<div id="main"></div>
16+
</body>
17+
</html>

src/domain/applications/handlers/checkin.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use dioxus::prelude::*;
22
use serde::{Deserialize, Serialize};
33

44
#[cfg(feature = "server")]
5-
use crate::core::auth::{context::RequestContext, middleware::SyncedUser};
5+
use crate::core::auth::{
6+
context::RequestContext, middleware::SyncedUser, permissions::Permissions,
7+
};
68
#[cfg(feature = "server")]
79
use utoipa::ToSchema;
810

@@ -162,12 +164,13 @@ pub async fn organizer_checkin(
162164

163165
let hackathon = ctx.hackathon()?;
164166

165-
// Verify user is organizer or admin
167+
// Verify user is organizer, admin, or global admin
168+
let is_global_admin = Permissions::is_global_admin(&ctx);
166169
let role_repo = UserRoleRepository::new(&ctx.state.db);
167170
let is_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
168171
let is_organizer = role_repo.is_organizer(ctx.user.id, hackathon.id).await?;
169172

170-
if !is_admin && !is_organizer {
173+
if !is_global_admin && !is_admin && !is_organizer {
171174
return Err(ServerFnError::new(
172175
"Only organizers can check in participants",
173176
));
@@ -236,12 +239,13 @@ pub async fn organizer_remove_checkin(
236239

237240
let hackathon = ctx.hackathon()?;
238241

239-
// Verify user is organizer or admin
242+
// Verify user is organizer, admin, or global admin
243+
let is_global_admin = Permissions::is_global_admin(&ctx);
240244
let role_repo = UserRoleRepository::new(&ctx.state.db);
241245
let is_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
242246
let is_organizer = role_repo.is_organizer(ctx.user.id, hackathon.id).await?;
243247

244-
if !is_admin && !is_organizer {
248+
if !is_global_admin && !is_admin && !is_organizer {
245249
return Err(ServerFnError::new("Only organizers can remove check-ins"));
246250
}
247251

@@ -284,12 +288,13 @@ pub async fn get_attendees(slug: String, event_id: i32) -> Result<Vec<Attendee>,
284288

285289
let hackathon = ctx.hackathon()?;
286290

287-
// Verify user is organizer or admin
291+
// Verify user is organizer, admin, or global admin
292+
let is_global_admin = Permissions::is_global_admin(&ctx);
288293
let role_repo = UserRoleRepository::new(&ctx.state.db);
289294
let is_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
290295
let is_organizer = role_repo.is_organizer(ctx.user.id, hackathon.id).await?;
291296

292-
if !is_admin && !is_organizer {
297+
if !is_global_admin && !is_admin && !is_organizer {
293298
return Err(ServerFnError::new("Only organizers can view attendees"));
294299
}
295300

@@ -412,12 +417,13 @@ pub async fn get_participant_info(
412417

413418
let hackathon = ctx.hackathon()?;
414419

415-
// Verify user is organizer or admin
420+
// Verify user is organizer, admin, or global admin
421+
let is_global_admin = Permissions::is_global_admin(&ctx);
416422
let role_repo = UserRoleRepository::new(&ctx.state.db);
417423
let is_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
418424
let is_organizer = role_repo.is_organizer(ctx.user.id, hackathon.id).await?;
419425

420-
if !is_admin && !is_organizer {
426+
if !is_global_admin && !is_admin && !is_organizer {
421427
return Err(ServerFnError::new(
422428
"Only organizers can look up participants",
423429
));

src/domain/applications/handlers/events.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use dioxus::prelude::*;
22

33
#[cfg(feature = "server")]
4-
use crate::core::auth::{context::RequestContext, middleware::SyncedUser};
4+
use crate::core::auth::{
5+
context::RequestContext, middleware::SyncedUser, permissions::Permissions,
6+
};
57

68
/// Get user schedule
79
#[cfg_attr(feature = "server", utoipa::path(
@@ -111,11 +113,12 @@ pub async fn create_event(
111113

112114
let hackathon = ctx.hackathon()?;
113115

114-
// Check if user is admin
116+
// Check if user is admin (global or hackathon-level)
117+
let is_global_admin = Permissions::is_global_admin(&ctx);
115118
let role_repo = UserRoleRepository::new(&ctx.state.db);
116-
let is_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
119+
let is_hackathon_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
117120

118-
if !is_admin {
121+
if !is_global_admin && !is_hackathon_admin {
119122
return Err(ServerFnError::new("Only admins can create events"));
120123
}
121124

@@ -231,11 +234,12 @@ pub async fn update_event(
231234

232235
let hackathon = ctx.hackathon()?;
233236

234-
// Check if user is admin
237+
// Check if user is admin (global or hackathon-level)
238+
let is_global_admin = Permissions::is_global_admin(&ctx);
235239
let role_repo = UserRoleRepository::new(&ctx.state.db);
236-
let is_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
240+
let is_hackathon_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
237241

238-
if !is_admin {
242+
if !is_global_admin && !is_hackathon_admin {
239243
return Err(ServerFnError::new("Only admins can update events"));
240244
}
241245

@@ -332,11 +336,12 @@ pub async fn delete_event(slug: String, id: i32) -> Result<(), ServerFnError> {
332336

333337
let hackathon = ctx.hackathon()?;
334338

335-
// Check if user is admin
339+
// Check if user is admin (global or hackathon-level)
340+
let is_global_admin = Permissions::is_global_admin(&ctx);
336341
let role_repo = UserRoleRepository::new(&ctx.state.db);
337-
let is_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
342+
let is_hackathon_admin = role_repo.is_admin(ctx.user.id, hackathon.id).await?;
338343

339-
if !is_admin {
344+
if !is_global_admin && !is_hackathon_admin {
340345
return Err(ServerFnError::new("Only admins can delete events"));
341346
}
342347

src/domain/hackathons/handlers/manifest.rs

Lines changed: 134 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -71,52 +71,14 @@ pub async fn get_manifest(
7171
let icons = if let Some(ref icon_url) = hackathon.app_icon_url {
7272
// When a custom icon is uploaded, use it for all sizes
7373
// The browser will scale it appropriately
74-
vec![
75-
ManifestIcon {
76-
src: icon_url.clone(),
77-
sizes: "512x512".to_string(),
78-
icon_type: guess_icon_type(icon_url),
79-
},
80-
ManifestIcon {
81-
src: icon_url.clone(),
82-
sizes: "192x192".to_string(),
83-
icon_type: guess_icon_type(icon_url),
84-
},
85-
ManifestIcon {
86-
src: icon_url.clone(),
87-
sizes: "144x144".to_string(),
88-
icon_type: guess_icon_type(icon_url),
89-
},
90-
ManifestIcon {
91-
src: icon_url.clone(),
92-
sizes: "96x96".to_string(),
93-
icon_type: guess_icon_type(icon_url),
94-
},
95-
]
74+
vec![ManifestIcon {
75+
src: icon_url.clone(),
76+
sizes: "512x512".to_string(),
77+
icon_type: guess_icon_type(icon_url),
78+
}]
9679
} else {
97-
// Fall back to default icons
98-
vec![
99-
ManifestIcon {
100-
src: "/th26_icons/android/android-launchericon-512-512.png".to_string(),
101-
sizes: "512x512".to_string(),
102-
icon_type: "image/png".to_string(),
103-
},
104-
ManifestIcon {
105-
src: "/th26_icons/android/android-launchericon-192-192.png".to_string(),
106-
sizes: "192x192".to_string(),
107-
icon_type: "image/png".to_string(),
108-
},
109-
ManifestIcon {
110-
src: "/th26_icons/android/android-launchericon-144-144.png".to_string(),
111-
sizes: "144x144".to_string(),
112-
icon_type: "image/png".to_string(),
113-
},
114-
ManifestIcon {
115-
src: "/th26_icons/android/android-launchericon-96-96.png".to_string(),
116-
sizes: "96x96".to_string(),
117-
icon_type: "image/png".to_string(),
118-
},
119-
]
80+
// Fall back to default icons (do not exist yet)
81+
vec![]
12082
};
12183

12284
// Build manifest with hackathon-specific data
@@ -159,3 +121,130 @@ fn guess_icon_type(url: &str) -> String {
159121
"image/png".to_string()
160122
}
161123
}
124+
125+
/// Serve manifest.json at root, extracting hackathon slug from Referer header
126+
pub async fn get_root_manifest(
127+
State(state): State<AppState>,
128+
headers: HeaderMap,
129+
) -> impl IntoResponse {
130+
// Try to extract hackathon slug from Referer header
131+
let slug = headers
132+
.get("referer")
133+
.and_then(|r| r.to_str().ok())
134+
.and_then(|referer| {
135+
// Parse URL like "https://example.com/h/th26/dashboard"
136+
// Extract the slug after "/h/"
137+
if let Some(start) = referer.find("/h/") {
138+
let rest = &referer[start + 3..];
139+
// Take until next "/" or end
140+
let slug = rest.split('/').next()?;
141+
if !slug.is_empty() {
142+
return Some(slug.to_string());
143+
}
144+
}
145+
None
146+
});
147+
148+
let Some(slug) = slug else {
149+
// No hackathon context, return a default/generic manifest
150+
let manifest = WebManifest {
151+
name: "Terrier".to_string(),
152+
short_name: "Terrier".to_string(),
153+
description: "Hackathon management platform".to_string(),
154+
start_url: "/".to_string(),
155+
scope: "/".to_string(),
156+
display: "fullscreen".to_string(),
157+
background_color: "#F4F2F3".to_string(),
158+
theme_color: "#F4F2F3".to_string(),
159+
orientation: "portrait".to_string(),
160+
icons: vec![],
161+
};
162+
163+
let mut headers = HeaderMap::new();
164+
headers.insert(
165+
"Content-Type",
166+
HeaderValue::from_static("application/manifest+json"),
167+
);
168+
return (headers, Json(manifest)).into_response();
169+
};
170+
171+
// Find the hackathon
172+
let hackathon = match hackathons::Entity::find()
173+
.filter(hackathons::Column::Slug.eq(&slug))
174+
.one(&state.db)
175+
.await
176+
{
177+
Ok(Some(h)) => h,
178+
Ok(None) => {
179+
// Fallback to generic manifest
180+
let manifest = WebManifest {
181+
name: "Terrier".to_string(),
182+
short_name: "Terrier".to_string(),
183+
description: "Hackathon management platform".to_string(),
184+
start_url: "/".to_string(),
185+
scope: "/".to_string(),
186+
display: "fullscreen".to_string(),
187+
background_color: "#F4F2F3".to_string(),
188+
theme_color: "#F4F2F3".to_string(),
189+
orientation: "portrait".to_string(),
190+
icons: vec![],
191+
};
192+
193+
let mut headers = HeaderMap::new();
194+
headers.insert(
195+
"Content-Type",
196+
HeaderValue::from_static("application/manifest+json"),
197+
);
198+
return (headers, Json(manifest)).into_response();
199+
}
200+
Err(e) => {
201+
tracing::error!("Failed to fetch hackathon: {:?}", e);
202+
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response();
203+
}
204+
};
205+
206+
let scope = format!("/h/{}/", slug);
207+
let start_url = format!("/h/{}/", slug);
208+
209+
let theme_color = hackathon
210+
.theme_color
211+
.clone()
212+
.unwrap_or_else(|| "#F4F2F3".to_string());
213+
let background_color = hackathon
214+
.background_color
215+
.clone()
216+
.unwrap_or_else(|| "#F4F2F3".to_string());
217+
218+
let icons = if let Some(ref icon_url) = hackathon.app_icon_url {
219+
vec![ManifestIcon {
220+
src: icon_url.clone(),
221+
sizes: "512x512".to_string(),
222+
icon_type: guess_icon_type(icon_url),
223+
}]
224+
} else {
225+
vec![]
226+
};
227+
228+
let manifest = WebManifest {
229+
name: hackathon.name.clone(),
230+
short_name: hackathon.name.clone(),
231+
description: hackathon
232+
.description
233+
.unwrap_or_else(|| format!("The app for {}!", hackathon.name)),
234+
start_url,
235+
scope,
236+
display: "fullscreen".to_string(),
237+
background_color,
238+
theme_color,
239+
orientation: "portrait".to_string(),
240+
icons,
241+
};
242+
243+
let mut headers = HeaderMap::new();
244+
headers.insert(
245+
"Content-Type",
246+
HeaderValue::from_static("application/manifest+json"),
247+
);
248+
249+
(headers, Json(manifest)).into_response()
250+
}

0 commit comments

Comments
 (0)