diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json
index e934339..e73eb3a 100644
--- a/src/inc/locales/de.json
+++ b/src/inc/locales/de.json
@@ -747,6 +747,7 @@
"delete_btn": "Löschen",
"delete_confirm": "Diesen Einladungstoken löschen?",
"slot_refreshes_on": "Slot erneuert sich am {date}",
- "slot_refreshed": "Slot erneuert"
+ "slot_refreshed": "Slot erneuert",
+ "admin_desc": "Du bist Admin, leg los."
}
}
\ No newline at end of file
diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json
index cbc2571..d7e9f39 100644
--- a/src/inc/locales/en.json
+++ b/src/inc/locales/en.json
@@ -749,6 +749,7 @@
"delete_btn": "Delete",
"delete_confirm": "Delete this invite token?",
"slot_refreshes_on": "slot refreshes on {date}",
- "slot_refreshed": "slot refreshed"
+ "slot_refreshed": "slot refreshed",
+ "admin_desc": "You are an admin, go ahead."
}
}
\ No newline at end of file
diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json
index 9acc3fe..464cc30 100644
--- a/src/inc/locales/nl.json
+++ b/src/inc/locales/nl.json
@@ -745,6 +745,7 @@
"delete_btn": "Verwijderen",
"delete_confirm": "Dit uitnodigingstoken verwijderen?",
"slot_refreshes_on": "slot vernieuwd op {date}",
- "slot_refreshed": "slot vernieuwd"
+ "slot_refreshed": "slot vernieuwd",
+ "admin_desc": "Je bent admin, ga je gang."
}
}
\ No newline at end of file
diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json
index e21c5e4..aa8fc82 100644
--- a/src/inc/locales/zange.json
+++ b/src/inc/locales/zange.json
@@ -750,6 +750,7 @@
"delete_btn": "Löschen",
"delete_confirm": "Diesen Einladungskot löschen?",
"slot_refreshes_on": "Platz erneuert sich am {date}",
- "slot_refreshed": "Platz erneuert"
+ "slot_refreshed": "Platz erneuert",
+ "admin_desc": "Du bist Admin, mach weiter."
}
}
\ No newline at end of file
diff --git a/src/inc/routes/apiv2/settings.mjs b/src/inc/routes/apiv2/settings.mjs
index 00bec46..dffbf70 100644
--- a/src/inc/routes/apiv2/settings.mjs
+++ b/src/inc/routes/apiv2/settings.mjs
@@ -842,29 +842,9 @@ export default router => {
const username = req.session.user;
const totalSlots = getInviteSlots();
const refreshDays = 30;
+ const isAdmin = !!req.session.admin;
- // Gather eligibility stats in one query
- const [stats] = await db`
- SELECT
- (SELECT COUNT(*) FROM items WHERE username = ${username} AND active = true AND is_deleted = false)::int AS upload_count,
- EXTRACT(EPOCH FROM (NOW() - u.created_at)) / 86400 AS age_days,
- (SELECT COUNT(*) FROM comments WHERE user_id = ${userId} AND is_deleted = false)::int AS comment_count,
- (SELECT COUNT(*) FROM tags_assign WHERE user_id = ${userId})::int AS tag_count
- FROM "user" u
- WHERE u.id = ${userId}
- `;
-
- const INVITE_CRITERIA = getInviteCriteria();
- const criteria = {
- uploads: { current: stats.upload_count, required: INVITE_CRITERIA.uploads, met: stats.upload_count >= INVITE_CRITERIA.uploads },
- age_days: { current: Math.floor(stats.age_days), required: INVITE_CRITERIA.age_days, met: stats.age_days >= INVITE_CRITERIA.age_days },
- comments: { current: stats.comment_count, required: INVITE_CRITERIA.comments, met: stats.comment_count >= INVITE_CRITERIA.comments },
- tags: { current: stats.tag_count, required: INVITE_CRITERIA.tags, met: stats.tag_count >= INVITE_CRITERIA.tags },
- };
-
- const eligible = Object.values(criteria).every(c => c.met);
-
- // Fetch all tokens this user created, join used_by name
+ // Always fetch this user's token history
const tokens = await db`
SELECT
it.id,
@@ -879,19 +859,50 @@ export default router => {
ORDER BY it.created_at DESC
`;
- // Slots consumed = tokens used within the last 30 days
- const cutoff = new Date(Date.now() - refreshDays * 24 * 60 * 60 * 1000);
- const slotsConsumed = tokens.filter(t => t.is_used && t.used_at && new Date(t.used_at) > cutoff).length;
- const slotsAvailable = Math.max(0, totalSlots - slotsConsumed);
+ let eligible, criteria, slotsConsumed, slotsAvailable;
+
+ if (isAdmin) {
+ // Admins bypass all criteria and slot limits
+ eligible = true;
+ criteria = null;
+ slotsConsumed = 0;
+ slotsAvailable = Infinity;
+ } else {
+ // Gather eligibility stats in one query
+ const [stats] = await db`
+ SELECT
+ (SELECT COUNT(*) FROM items WHERE username = ${username} AND active = true AND is_deleted = false)::int AS upload_count,
+ EXTRACT(EPOCH FROM (NOW() - u.created_at)) / 86400 AS age_days,
+ (SELECT COUNT(*) FROM comments WHERE user_id = ${userId} AND is_deleted = false)::int AS comment_count,
+ (SELECT COUNT(*) FROM tags_assign WHERE user_id = ${userId})::int AS tag_count
+ FROM "user" u
+ WHERE u.id = ${userId}
+ `;
+
+ const INVITE_CRITERIA = getInviteCriteria();
+ criteria = {
+ uploads: { current: stats.upload_count, required: INVITE_CRITERIA.uploads, met: stats.upload_count >= INVITE_CRITERIA.uploads },
+ age_days: { current: Math.floor(stats.age_days), required: INVITE_CRITERIA.age_days, met: stats.age_days >= INVITE_CRITERIA.age_days },
+ comments: { current: stats.comment_count, required: INVITE_CRITERIA.comments, met: stats.comment_count >= INVITE_CRITERIA.comments },
+ tags: { current: stats.tag_count, required: INVITE_CRITERIA.tags, met: stats.tag_count >= INVITE_CRITERIA.tags },
+ };
+ eligible = Object.values(criteria).every(c => c.met);
+
+ // Slots consumed = tokens used within the last 30 days
+ const cutoff = new Date(Date.now() - refreshDays * 24 * 60 * 60 * 1000);
+ slotsConsumed = tokens.filter(t => t.is_used && t.used_at && new Date(t.used_at) > cutoff).length;
+ slotsAvailable = Math.max(0, totalSlots - slotsConsumed);
+ }
return res.json({
success: true,
+ is_admin: isAdmin,
eligible,
criteria,
tokens,
- slots_total: totalSlots,
- slots_consumed: slotsConsumed,
- slots_available: slotsAvailable,
+ slots_total: isAdmin ? null : totalSlots,
+ slots_consumed: isAdmin ? null : slotsConsumed,
+ slots_available: isAdmin ? null : slotsAvailable,
refresh_days: refreshDays,
}, 200);
} catch (e) {
@@ -912,39 +923,43 @@ export default router => {
const totalSlots = getInviteSlots();
const refreshDays = 30;
- // Eligibility check
- const [stats] = await db`
- SELECT
- (SELECT COUNT(*) FROM items WHERE username = ${username} AND active = true AND is_deleted = false)::int AS upload_count,
- EXTRACT(EPOCH FROM (NOW() - u.created_at)) / 86400 AS age_days,
- (SELECT COUNT(*) FROM comments WHERE user_id = ${userId} AND is_deleted = false)::int AS comment_count,
- (SELECT COUNT(*) FROM tags_assign WHERE user_id = ${userId})::int AS tag_count
- FROM "user" u WHERE u.id = ${userId}
- `;
+ const isAdmin = !!req.session.admin;
- const INVITE_CRITERIA = getInviteCriteria();
- const eligible =
- stats.upload_count >= INVITE_CRITERIA.uploads &&
- stats.age_days >= INVITE_CRITERIA.age_days &&
- stats.comment_count >= INVITE_CRITERIA.comments &&
- stats.tag_count >= INVITE_CRITERIA.tags;
+ if (!isAdmin) {
+ // Eligibility check
+ const [stats] = await db`
+ SELECT
+ (SELECT COUNT(*) FROM items WHERE username = ${username} AND active = true AND is_deleted = false)::int AS upload_count,
+ EXTRACT(EPOCH FROM (NOW() - u.created_at)) / 86400 AS age_days,
+ (SELECT COUNT(*) FROM comments WHERE user_id = ${userId} AND is_deleted = false)::int AS comment_count,
+ (SELECT COUNT(*) FROM tags_assign WHERE user_id = ${userId})::int AS tag_count
+ FROM "user" u WHERE u.id = ${userId}
+ `;
- if (!eligible) {
- return res.json({ success: false, msg: 'You do not meet the eligibility criteria' }, 403);
- }
+ const INVITE_CRITERIA = getInviteCriteria();
+ const eligible =
+ stats.upload_count >= INVITE_CRITERIA.uploads &&
+ stats.age_days >= INVITE_CRITERIA.age_days &&
+ stats.comment_count >= INVITE_CRITERIA.comments &&
+ stats.tag_count >= INVITE_CRITERIA.tags;
- // Check available slots (used within last 30 days)
- const cutoff = new Date(Date.now() - refreshDays * 24 * 60 * 60 * 1000);
- const [{ slots_consumed }] = await db`
- SELECT COUNT(*)::int AS slots_consumed
- FROM invite_tokens
- WHERE created_by = ${userId}
- AND is_used = true
- AND used_at > ${cutoff}
- `;
+ if (!eligible) {
+ return res.json({ success: false, msg: 'You do not meet the eligibility criteria' }, 403);
+ }
- if (slots_consumed >= totalSlots) {
- return res.json({ success: false, msg: 'No invite slots available. Slots refresh 30 days after use.' }, 403);
+ // Check available slots (used within last 30 days)
+ const cutoff = new Date(Date.now() - refreshDays * 24 * 60 * 60 * 1000);
+ const [{ slots_consumed }] = await db`
+ SELECT COUNT(*)::int AS slots_consumed
+ FROM invite_tokens
+ WHERE created_by = ${userId}
+ AND is_used = true
+ AND used_at > ${cutoff}
+ `;
+
+ if (slots_consumed >= totalSlots) {
+ return res.json({ success: false, msg: 'No invite slots available. Slots refresh 30 days after use.' }, 403);
+ }
}
// Generate token
diff --git a/views/settings.html b/views/settings.html
index c19b0ff..60e743a 100644
--- a/views/settings.html
+++ b/views/settings.html
@@ -3,7 +3,76 @@
{{ t('settings.title') }}
-
{{ t('settings.avatar') }}
+
+
+
+ {{ t('settings.avatar') }}
+ {{ t('settings.preferences') }}
+ @if(enable_data_export)
+ {{ t('settings.export_data_title') }}
+ @endif
+ {{ t('settings.account') }}
+ @if(matrix_enabled)
+ {{ t('settings.linked_accounts') }}
+ @endif
+ @if(enable_user_api_keys)
+ API Key
+ @endif
+ @if(enable_user_invites)
+ {{ t('invites.section_title') }}
+ @endif
+
+
+
+
+
{{ t('settings.avatar') }}
{{ t('settings.current_avatar') }}
@@ -75,7 +144,7 @@
{{ t('settings.username_color_hint') }}
-
{{ t('settings.preferences') }}
+
{{ t('settings.preferences') }}
@@ -298,7 +367,7 @@
@if(enable_data_export)
-
{{ t('settings.export_data_title') || 'Export Data' }}
+
{{ t('settings.export_data_title') || 'Export Data' }}
{{ t('settings.export_data_desc') || 'Download a copy of your data. This process happens entirely in your browser to protect your privacy and save server resources.' }}
@@ -339,7 +408,7 @@
@endif
-
{{ t('settings.account') }}
+
{{ t('settings.account') }}
@if(matrix_enabled)
- {{ t('settings.linked_accounts') }}
+ {{ t('settings.linked_accounts') }}
{{ t('settings.matrix_link_desc') }}
@@ -441,7 +510,7 @@
@endif
@if(enable_user_api_keys)
-
Upload API Key
+
Upload API Key
@@ -475,11 +544,11 @@
@endif
@if(enable_user_invites)
-
{{ t('invites.section_title') }}
+
{{ t('invites.section_title') }}
-
{{ t('invites.section_desc') }}
+
{{ t('invites.section_desc') }}
@@ -584,6 +653,7 @@
data-slot-refreshes-on="{{ t('invites.slot_refreshes_on') }}"
data-slot-refreshed="{{ t('invites.slot_refreshed') }}"
data-generating="{{ t('invites.generating') }}"
+ data-admin-desc="{{ t('invites.admin_desc') }}"
>