Skip to content

Commit 4ce1e89

Browse files
committed
Update password reset redirect URLs
- Combine users and roles admin pages - Make Containers the default tab on network
1 parent 7991529 commit 4ce1e89

File tree

10 files changed

+432
-466
lines changed

10 files changed

+432
-466
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
<script setup lang="ts">
2+
import { Alert, Badge, Card, Flex, Grid, Skeleton } from '@dolanske/vui'
3+
4+
import RoleKPIs from '@/components/Admin/Roles/RoleKPIs.vue'
5+
import { useBreakpoint } from '@/lib/mediaQuery'
6+
7+
const isBelowMedium = useBreakpoint('<m')
8+
9+
const supabase = useSupabaseClient()
10+
11+
const loading = ref(true)
12+
const errorMessage = ref('')
13+
const rolePermissions = ref<{ role: string, permission: string }[]>([])
14+
const refreshSignal = ref(0)
15+
16+
const defaultUserPermissions = [
17+
'announcements.read',
18+
'events.read',
19+
'games.read',
20+
'gameservers.read',
21+
'profiles.read',
22+
'referendums.read',
23+
'roles.read',
24+
'profiles.update.own',
25+
'complaints.create.own',
26+
'complaints.read.own',
27+
'referendum_votes.create',
28+
'referendum_votes.update.own',
29+
'referendum_votes.delete.own',
30+
]
31+
32+
const permissionsByRole = computed(() => {
33+
const grouped: Record<string, string[]> = {}
34+
35+
rolePermissions.value.forEach(({ role, permission }) => {
36+
if (!grouped[role]) {
37+
grouped[role] = []
38+
}
39+
grouped[role].push(permission)
40+
})
41+
42+
grouped.user = [...defaultUserPermissions]
43+
44+
Object.keys(grouped).forEach((role) => {
45+
if (grouped[role]) {
46+
grouped[role].sort()
47+
}
48+
})
49+
50+
return grouped
51+
})
52+
53+
const groupedPermissions = computed(() => {
54+
const result: Record<string, Record<string, string[]>> = {}
55+
56+
Object.entries(permissionsByRole.value).forEach(([role, permissions]) => {
57+
const roleGroups = result[role] ?? (result[role] = {})
58+
59+
permissions.forEach((permission) => {
60+
const [category] = permission.split('.')
61+
if (!category)
62+
return
63+
64+
if (!roleGroups[category]) {
65+
roleGroups[category] = []
66+
}
67+
roleGroups[category].push(permission)
68+
})
69+
})
70+
71+
return result
72+
})
73+
74+
const availableRoles = ['admin', 'moderator', 'user']
75+
76+
async function fetchRolePermissions() {
77+
loading.value = true
78+
errorMessage.value = ''
79+
80+
try {
81+
const { data, error } = await supabase
82+
.from('role_permissions')
83+
.select('role, permission')
84+
.order('role')
85+
.order('permission')
86+
87+
if (error)
88+
throw error
89+
90+
rolePermissions.value = data || []
91+
}
92+
catch (error: unknown) {
93+
errorMessage.value = (error as Error).message || 'Failed to fetch role permissions'
94+
}
95+
finally {
96+
loading.value = false
97+
}
98+
}
99+
100+
function formatPermissionName(permission: string): string {
101+
const parts = permission.split('.')
102+
const [category, action, scope] = parts
103+
104+
if (scope === 'own' && action) {
105+
return `${action.charAt(0).toUpperCase() + action.slice(1)} own ${category}`
106+
}
107+
108+
if (permission === 'referendum_votes.create')
109+
return 'Vote on referendums'
110+
if (permission === 'referendum_votes.update.own')
111+
return 'Update own votes'
112+
if (permission === 'referendum_votes.delete.own')
113+
return 'Delete own votes'
114+
115+
if (permission === 'profiles.update.own')
116+
return 'Update own profile'
117+
if (permission === 'complaints.create.own')
118+
return 'Create own complaints'
119+
if (permission === 'complaints.read.own')
120+
return 'View own complaints'
121+
122+
if (action) {
123+
return `${action.charAt(0).toUpperCase() + action.slice(1)} ${category}`
124+
}
125+
return category || permission
126+
}
127+
128+
function formatCategoryName(category: string): string {
129+
return category.charAt(0).toUpperCase() + category.slice(1)
130+
}
131+
132+
function getRoleColor(role: string): string {
133+
switch (role) {
134+
case 'admin':
135+
return 'var(--color-text-red)'
136+
case 'moderator':
137+
return 'var(--color-text-blue)'
138+
case 'user':
139+
return 'var(--color-text-green)'
140+
default:
141+
return 'var(--color-text)'
142+
}
143+
}
144+
145+
function getCategoryIcon(category: string): string {
146+
const icons: Record<string, string> = {
147+
announcements: 'ph:megaphone',
148+
complaints: 'ph:warning-circle',
149+
containers: 'ph:cube',
150+
events: 'ph:calendar',
151+
expenses: 'ph:receipt',
152+
forums: 'ph:chat-circle',
153+
funding: 'ph:coins',
154+
games: 'ph:game-controller',
155+
gameservers: 'ph:computer-tower',
156+
profiles: 'ph:user-circle',
157+
referendums: 'ph:user-sound',
158+
referendum_votes: 'ph:hand-pointing',
159+
roles: 'ph:shield-check',
160+
servers: 'ph:database',
161+
users: 'ph:user',
162+
}
163+
return icons[category] || 'ph:circle'
164+
}
165+
166+
onBeforeMount(fetchRolePermissions)
167+
</script>
168+
169+
<template>
170+
<!-- KPIs Section -->
171+
<RoleKPIs v-model:refresh-signal="refreshSignal" />
172+
173+
<!-- Loading state -->
174+
<template v-if="loading">
175+
<Grid :columns="isBelowMedium ? 1 : 3" gap="l" expand>
176+
<template v-for="i in 3" :key="i">
177+
<Card expand class="role-card">
178+
<template #header>
179+
<Flex x-between y-center>
180+
<Flex y-center gap="s">
181+
<Skeleton :width="80" :height="28" :radius="4" />
182+
<Skeleton :width="120" :height="24" :radius="12" />
183+
</Flex>
184+
<Skeleton :width="24" :height="24" :radius="4" />
185+
</Flex>
186+
</template>
187+
188+
<Flex column gap="m">
189+
<template v-for="j in (i === 1 ? 8 : i === 2 ? 6 : 4)" :key="j">
190+
<Flex y-center gap="xs" expand>
191+
<Skeleton :width="16" :height="16" :radius="2" />
192+
<Skeleton :width="100" :height="20" :radius="4" />
193+
<Skeleton :width="30" :height="16" :radius="4" />
194+
</Flex>
195+
196+
<Flex class="permissions-list" style="padding-right: 24px;" gap="s" expand column>
197+
<template v-for="k in (Math.floor(Math.random() * 3) + 2)" :key="k">
198+
<Flex y-center gap="xs" class="permission-item">
199+
<Skeleton :width="12" :height="12" :radius="50" />
200+
<Skeleton :height="16" :radius="4" />
201+
</Flex>
202+
</template>
203+
</Flex>
204+
</template>
205+
</Flex>
206+
</Card>
207+
</template>
208+
</Grid>
209+
</template>
210+
211+
<Alert v-else-if="errorMessage" variant="danger">
212+
{{ errorMessage }}
213+
</Alert>
214+
215+
<template v-else>
216+
<Grid :columns="isBelowMedium ? 1 : 3" gap="l" expand>
217+
<Card v-for="role in availableRoles" :key="role" expand class="role-card">
218+
<template #header>
219+
<Flex x-between y-center>
220+
<Flex y-center gap="s">
221+
<h3 class="role-title" :style="{ color: getRoleColor(role) }">
222+
{{ role.charAt(0).toUpperCase() + role.slice(1) }}
223+
</h3>
224+
<Badge
225+
:style="{ backgroundColor: getRoleColor(role),
226+
color: 'white' }"
227+
>
228+
{{ permissionsByRole[role]?.length || 0 }} permissions
229+
</Badge>
230+
</Flex>
231+
<Icon name="ph:shield-check" size="1.5rem" :style="{ color: getRoleColor(role) }" />
232+
</Flex>
233+
</template>
234+
235+
<div v-if="groupedPermissions[role]" class="permissions-container">
236+
<div v-for="(permissions, category) in groupedPermissions[role]" :key="category" class="permission-category">
237+
<Flex y-center gap="xs" class="category-header">
238+
<Icon :name="getCategoryIcon(category)" size="1rem" class="category-icon" />
239+
<h4 class="category-title">
240+
{{ formatCategoryName(category) }}
241+
</h4>
242+
<span class="text-color-light text-xs">({{ permissions.length }})</span>
243+
</Flex>
244+
245+
<div class="permissions-list">
246+
<div v-for="permission in permissions" :key="permission" class="permission-item">
247+
<Icon name="ph:check-circle" size="0.8rem" class="permission-check" />
248+
<span class="permission-text">{{ formatPermissionName(permission) }}</span>
249+
</div>
250+
</div>
251+
</div>
252+
</div>
253+
254+
<div v-else class="no-permissions">
255+
<Icon name="ph:warning-circle" size="1.2rem" class="text-color-light" />
256+
<span class="text-color-light">No permissions assigned</span>
257+
</div>
258+
</Card>
259+
</Grid>
260+
</template>
261+
</template>
262+
263+
<style scoped lang="scss">
264+
.role-card {
265+
position: relative;
266+
overflow: hidden;
267+
}
268+
269+
.role-title {
270+
font-size: var(--font-size-xl);
271+
font-weight: var(--font-weight-semibold);
272+
margin: 0;
273+
}
274+
275+
.permissions-container {
276+
display: flex;
277+
flex-direction: column;
278+
gap: var(--space-m);
279+
}
280+
281+
.permission-category {
282+
display: flex;
283+
flex-direction: column;
284+
gap: var(--space-xs);
285+
}
286+
287+
.category-header {
288+
margin-bottom: var(--space-xs);
289+
}
290+
291+
.category-icon {
292+
color: var(--color-accent);
293+
}
294+
295+
.category-title {
296+
font-size: var(--font-size-m);
297+
font-weight: var(--font-weight-medium);
298+
margin: 0;
299+
color: var(--color-text);
300+
}
301+
302+
.permissions-list {
303+
display: flex;
304+
flex-direction: column;
305+
gap: var(--space-xs);
306+
margin-left: var(--space-l);
307+
}
308+
309+
.permission-item {
310+
width: 100% !important;
311+
display: flex;
312+
align-items: center;
313+
gap: var(--space-xs);
314+
padding: var(--space-xs);
315+
background: var(--color-bg-raised);
316+
border-radius: var(--border-radius-s);
317+
}
318+
319+
.permission-check {
320+
color: var(--color-success);
321+
flex-shrink: 0;
322+
}
323+
324+
.permission-text {
325+
font-size: var(--font-size-s);
326+
color: var(--color-text);
327+
}
328+
329+
.no-permissions {
330+
display: flex;
331+
align-items: center;
332+
gap: var(--space-s);
333+
padding: var(--space-m);
334+
background: var(--color-bg-raised);
335+
border-radius: var(--border-radius-m);
336+
text-align: center;
337+
justify-content: center;
338+
}
339+
340+
@media (max-width: 768px) {
341+
.permissions-list {
342+
margin-left: var(--space-m);
343+
}
344+
}
345+
</style>

app/components/Settings/ChangePasswordCard.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ async function sendPasswordReset() {
2828
2929
const redirectUrl = import.meta.client
3030
? (process.env.NODE_ENV === 'development'
31-
? 'http://localhost:3000/auth/confirm'
32-
: `${window.location.origin}/auth/confirm`)
31+
? 'http://localhost:3000/auth/confirm-password'
32+
: `${window.location.origin}/auth/confirm-password`)
3333
: undefined
3434
3535
const { error } = await supabase.auth.resetPasswordForEmail(user.value.email, redirectUrl ? { redirectTo: redirectUrl } : undefined)

app/layouts/admin.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,7 @@ const menuItems = [
203203
permissions: ['referendums.read', 'referendums.create', 'referendums.update', 'referendums.delete'],
204204
},
205205
{
206-
name: 'Roles',
207-
path: '/admin/roles',
208-
icon: 'ph:shield-check',
209-
permissions: ['roles.read'],
210-
},
211-
{
212-
name: 'Users',
206+
name: 'Users & Roles',
213207
path: '/admin/users',
214208
icon: 'ph:user',
215209
permissions: ['users.read', 'users.create', 'users.update', 'users.delete', 'profiles.read', 'profiles.update', 'profiles.delete'],

app/middleware/mfa-guard.global.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalizedLoade
4444
if (needsAal2 && !authRoutes.includes(to.path)) {
4545
return navigateTo({
4646
path: '/auth/sign-in',
47-
query: { ...to.query, mfa: '1' },
47+
query: { mfa: '1', redirect: to.fullPath },
4848
replace: true,
4949
})
5050
}

app/pages/admin/network.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ const canReadContainers = computed(() => hasPermission('containers.read'))
3131
// Tab management - compute available tabs and set default
3232
const availableTabs = computed(() => {
3333
const tabs = []
34-
if (canReadServers.value)
35-
tabs.push({ label: 'Servers', value: 'Servers' })
36-
if (canReadGameservers.value)
37-
tabs.push({ label: 'Gameservers', value: 'Gameservers' })
3834
if (canReadContainers.value)
3935
tabs.push({ label: 'Containers', value: 'Containers' })
36+
if (canReadGameservers.value)
37+
tabs.push({ label: 'Gameservers', value: 'Gameservers' })
38+
if (canReadServers.value)
39+
tabs.push({ label: 'Servers', value: 'Servers' })
4040
return tabs
4141
})
4242

0 commit comments

Comments
 (0)