Skip to content

Commit f938b6c

Browse files
authored
feat: admin page (#685)
* feat: create admin page & auth * feat: files managing download & display * feat: changeset
1 parent cd346b3 commit f938b6c

File tree

15 files changed

+543
-7
lines changed

15 files changed

+543
-7
lines changed

.changeset/loud-otters-hide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"enspire": minor
3+
---
4+
5+
admin page

app/components/custom/sidebar.vue

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
<script setup lang="ts">
22
import type { AllClubs } from '@@/types/api/user/all_clubs'
3+
import type { getRoleResponse } from '~~/utils/user-roles'
34
import { useClerk, useUser } from 'vue-clerk'
5+
import { isAdmin } from '~~/utils/user-roles'
46
57
const { user } = useUser()
68
const config = useRuntimeConfig()
9+
const route = useRoute()
710
811
const clerk = useClerk()
912
@@ -14,10 +17,17 @@ function signOutHandler() {
1417
const isPresidentOrVicePresident = ref(false)
1518
useState('isEnspireLoading').value = true
1619
17-
const { data: clubs, suspense } = useQuery<AllClubs>({
20+
const { data: clubs, suspense: __s1 } = useQuery<AllClubs>({
1821
queryKey: ['/api/user/all_clubs'],
1922
})
20-
await suspense()
23+
await __s1()
24+
25+
const isUserAdmin = ref(false)
26+
const { data: roleData, suspense: __s2 } = useQuery<getRoleResponse>({
27+
queryKey: ['/api/user/check-role'],
28+
})
29+
await __s2()
30+
isUserAdmin.value = isAdmin(roleData.value)
2131
2232
if (clubs.value) {
2333
useState('isEnspireLoading').value = false
@@ -102,6 +112,13 @@ const sidebarData = ref({
102112
]
103113
: []),
104114
],
115+
admin: [
116+
{
117+
name: '社团文件',
118+
url: '/admin/manage-files',
119+
icon: 'lucide:lock-keyhole',
120+
},
121+
],
105122
})
106123
</script>
107124

@@ -171,7 +188,7 @@ const sidebarData = ref({
171188
v-for="item in sidebarData.school"
172189
:key="item.name"
173190
class="rounded"
174-
:class="{ 'bg-foreground/10': $route.path === item.url }"
191+
:class="{ 'bg-foreground/10': route.path === item.url }"
175192
>
176193
<SidebarMenuButton as-child>
177194
<NuxtLink :href="item.url">
@@ -206,7 +223,7 @@ const sidebarData = ref({
206223
:key="subItem.title"
207224
class="flex items-center"
208225
>
209-
<div v-if="$route.path === subItem.url" class="border-text mr-2 h-4 w-1 border-l-2 border-foreground rounded -ml-3" />
226+
<div v-if="route.path === subItem.url" class="border-text mr-2 h-4 w-1 border-l-2 border-foreground rounded -ml-3" />
210227
<SidebarMenuSubButton as-child>
211228
<NuxtLink :href="subItem.url">
212229
<span>{{ subItem.title }}</span>
@@ -219,6 +236,24 @@ const sidebarData = ref({
219236
</Collapsible>
220237
</SidebarMenu>
221238
</SidebarGroup>
239+
<SidebarGroup v-if="isUserAdmin" class="group-data-[collapsible=icon]:hidden">
240+
<SidebarGroupLabel>管理</SidebarGroupLabel>
241+
<SidebarMenu>
242+
<SidebarMenuItem
243+
v-for="item in sidebarData.admin"
244+
:key="item.name"
245+
class="rounded"
246+
:class="{ 'bg-foreground/10': route.path === item.url }"
247+
>
248+
<SidebarMenuButton as-child>
249+
<NuxtLink :href="item.url">
250+
<Icon :name="item.icon" size="1.1em" />
251+
<span>{{ item.name }}</span>
252+
</NuxtLink>
253+
</SidebarMenuButton>
254+
</SidebarMenuItem>
255+
</SidebarMenu>
256+
</SidebarGroup>
222257
</SidebarContent>
223258
<SidebarFooter>
224259
<SidebarMenu>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script setup lang="ts">
2+
import { cn } from '@/lib/utils'
3+
import {
4+
ProgressIndicator,
5+
ProgressRoot,
6+
type ProgressRootProps,
7+
} from 'radix-vue'
8+
import { computed, type HTMLAttributes } from 'vue'
9+
10+
const props = withDefaults(
11+
defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(),
12+
{
13+
modelValue: 0,
14+
},
15+
)
16+
17+
const delegatedProps = computed(() => {
18+
const { class: _, ...delegated } = props
19+
20+
return delegated
21+
})
22+
</script>
23+
24+
<template>
25+
<ProgressRoot
26+
v-bind="delegatedProps"
27+
:class="
28+
cn(
29+
'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
30+
props.class,
31+
)
32+
"
33+
>
34+
<ProgressIndicator
35+
class="h-full w-full flex-1 bg-primary transition-all"
36+
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
37+
/>
38+
</ProgressRoot>
39+
</template>

app/components/ui/progress/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Progress } from './Progress.vue'

app/middleware/admin.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Roles } from '@prisma/client'
2+
import { until } from '@vueuse/core'
3+
import { useClerk, useClerkProvider } from 'vue-clerk'
4+
import { getRole, isAdmin } from '~~/utils/user-roles'
5+
6+
export default defineNuxtRouteMiddleware(async () => {
7+
// Modified from auth.ts
8+
9+
const nuxtApp = useNuxtApp()
10+
const clerk = useClerk()
11+
const { isClerkLoaded } = useClerkProvider()
12+
13+
if (import.meta.client) {
14+
if (nuxtApp.isHydrating && nuxtApp.payload.serverRendered)
15+
return
16+
17+
await until(isClerkLoaded).toBe(true)
18+
if (clerk.loaded && clerk.user?.id == null)
19+
return navigateTo('/sign-in')
20+
const response = await $fetch('/api/user/check-role')
21+
if (!isAdmin(response))
22+
return abortNavigation()
23+
}
24+
25+
if (import.meta.server) {
26+
const id = nuxtApp.ssrContext?.event.context.auth?.userId
27+
if (id == null)
28+
return navigateTo('/sign-in')
29+
const response = await getRole(id)
30+
if (!isAdmin(response))
31+
return abortNavigation()
32+
}
33+
})

0 commit comments

Comments
 (0)