Skip to content

Commit 7d6f77b

Browse files
authored
feat: throw 401 errors when a user doesn't have permissions (#5984)
* feat: throw 401 errors when a user doesn't have permissions * remove pointless message * prepr
1 parent b538879 commit 7d6f77b

9 files changed

Lines changed: 236 additions & 98 deletions

File tree

apps/frontend/src/error.vue

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,52 @@
77
<Logo404 />
88
</div>
99
<div class="error-box" :class="{ 'has-bot': !is404 }">
10-
<img v-if="!is404" :src="SadRinthbot" alt="Sad Modrinth bot" class="error-box__sad-bot" />
10+
<img
11+
v-if="is401"
12+
:src="AnnoyedRinthbot"
13+
alt="Annoyed Modrinth bot"
14+
class="error-box__sad-bot"
15+
/>
16+
<img
17+
v-else-if="!is404"
18+
:src="SadRinthbot"
19+
alt="Sad Modrinth bot"
20+
class="error-box__sad-bot"
21+
/>
1122
<div v-if="!is404" class="error-box__top-glow" />
1223
<div class="error-box__body">
1324
<h1 class="error-box__title">{{ formatMessage(errorMessages.title) }}</h1>
1425
<p v-if="errorMessages.subtitle" class="error-box__subtitle">
1526
{{ formatMessage(errorMessages.subtitle) }}
1627
</p>
1728
</div>
29+
<div v-if="is401" class="flex flex-col gap-4">
30+
<template v-if="auth.user">
31+
<p class="m-0">
32+
{{ formatMessage(unauthorizedMessages.signedInAsLabel) }}
33+
</p>
34+
<div
35+
class="flex items-center gap-2 rounded-2xl border border-solid border-surface-5 bg-surface-4 p-4"
36+
>
37+
<Avatar :src="auth.user.avatar_url" size="32px" />
38+
<span class="font-medium text-contrast">{{ auth.user.username }}</span>
39+
40+
<ButtonStyled color="red" type="transparent">
41+
<button type="button" class="ml-auto" @click="logout">
42+
{{ formatMessage(commonMessages.signOutButton) }}
43+
</button>
44+
</ButtonStyled>
45+
</div>
46+
</template>
47+
<template v-else>
48+
<ButtonStyled color="brand">
49+
<nuxt-link class="button-like w-fit" :to="signInRoute">
50+
<LogInIcon />
51+
{{ formatMessage(commonMessages.signInButton) }}
52+
</nuxt-link>
53+
</ButtonStyled>
54+
</template>
55+
</div>
1856
<div class="error-box__body">
1957
<p v-if="errorMessages.list_title" class="error-box__list-title">
2058
{{ formatMessage(errorMessages.list_title) }}
@@ -51,9 +89,13 @@
5189
</template>
5290

5391
<script setup>
54-
import { SadRinthbot } from '@modrinth/assets'
92+
import { AnnoyedRinthbot, LogInIcon, SadRinthbot } from '@modrinth/assets'
5593
import {
94+
Avatar,
95+
ButtonStyled,
96+
commonMessages,
5697
defineMessage,
98+
defineMessages,
5799
IntlFormatted,
58100
LoadingBar,
59101
normalizeChildren,
@@ -65,6 +107,8 @@ import {
65107
} from '@modrinth/ui'
66108
67109
import Logo404 from '~/assets/images/404.svg'
110+
import { getSignInRouteObj } from '~/composables/auth.js'
111+
import { logout } from '~/composables/user.js'
68112
69113
import { createModrinthClient } from './helpers/api.ts'
70114
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
@@ -103,6 +147,17 @@ const props = defineProps({
103147
})
104148
105149
const is404 = computed(() => props.error.statusCode === 404)
150+
const is401 = computed(() => props.error.statusCode === 401)
151+
152+
const unauthorizedMessages = defineMessages({
153+
signedInAsLabel: {
154+
id: 'error.generic.401.signed-in-as',
155+
defaultMessage: "You're currently signed in as:",
156+
},
157+
})
158+
159+
const signInRoute = computed(() => getSignInRouteObj(route))
160+
106161
const errorMessages = computed(
107162
() =>
108163
routeMessages.find((x) => x.match(route))?.messages[props.error.statusCode] ??
@@ -112,10 +167,6 @@ const errorMessages = computed(
112167
113168
const route = useRoute()
114169
115-
watch(route, () => {
116-
console.log(route)
117-
})
118-
119170
const messages = {
120171
404: {
121172
title: defineMessage({
@@ -138,6 +189,12 @@ const messages = {
138189
'This page has been blocked for legal reasons, such as government censorship or ongoing legal proceedings.',
139190
}),
140191
},
192+
401: {
193+
title: defineMessage({
194+
id: 'error.generic.401.title',
195+
defaultMessage: `You don't have access to this page`,
196+
}),
197+
},
141198
default: {
142199
title: defineMessage({
143200
id: 'error.generic.default.title',
@@ -345,7 +402,7 @@ const routeMessages = [
345402
margin: 0;
346403
}
347404
348-
a {
405+
a:not(.button-like) {
349406
color: var(--color-brand);
350407
font-weight: 600;
351408
@@ -387,20 +444,24 @@ const routeMessages = [
387444
}
388445
389446
&__title {
390-
font-size: 2rem;
391-
font-weight: 900;
447+
font-size: 1.5rem;
448+
font-weight: 600;
392449
margin: 0;
393450
}
394451
395452
&__subtitle {
396-
font-size: 1.25rem;
397-
font-weight: 600;
453+
font-size: 1rem;
454+
font-weight: 400;
398455
}
399456
400457
&__body {
401458
display: flex;
402459
flex-direction: column;
403460
gap: 0.75rem;
461+
462+
&:empty {
463+
display: none;
464+
}
404465
}
405466
406467
&__list-title {

apps/frontend/src/layouts/default.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@
311311
{
312312
id: 'review-projects',
313313
color: 'orange',
314-
link: '/moderation/',
314+
link: '/moderation',
315315
},
316316
{
317317
id: 'tech-review',

apps/frontend/src/locales/en-US/index.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,12 @@
12441244
"error.collection.404.title": {
12451245
"message": "Collection not found"
12461246
},
1247+
"error.generic.401.signed-in-as": {
1248+
"message": "You're currently signed in as:"
1249+
},
1250+
"error.generic.401.title": {
1251+
"message": "You don't have access to this page"
1252+
},
12471253
"error.generic.404.subtitle": {
12481254
"message": "The page you were looking for doesn't seem to exist."
12491255
},
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { isStaff } from '@modrinth/utils'
2+
3+
export default defineNuxtRouteMiddleware(async () => {
4+
const auth = await useAuth()
5+
6+
if (!auth.value.user || !isStaff(auth.value.user)) {
7+
throw createError({
8+
fatal: true,
9+
statusCode: 401,
10+
statusMessage: 'Unauthorized',
11+
})
12+
}
13+
})

apps/frontend/src/pages/[type]/[id].vue

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,49 @@
33
<Teleport v-if="flags.projectBackground" to="#fixed-background-teleport">
44
<ProjectBackgroundGradient :project="project" />
55
</Teleport>
6-
<div v-if="route.name.startsWith('type-id-settings')" class="normal-page no-sidebar">
7-
<div class="normal-page__header">
8-
<div
9-
class="mb-4 flex flex-wrap items-center gap-x-2 gap-y-3 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold"
10-
>
11-
<nuxt-link
12-
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
13-
class="flex items-center gap-2 hover:underline hover:brightness-[--hover-brightness]"
6+
<template v-if="isSettings">
7+
<div v-if="canAccessSettings" class="normal-page no-sidebar">
8+
<div class="normal-page__header">
9+
<div
10+
class="mb-4 flex flex-wrap items-center gap-x-2 gap-y-3 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold"
1411
>
15-
<Avatar :src="project.icon_url" size="32px" />
16-
{{ project.title }}
17-
</nuxt-link>
18-
<ChevronRightIcon />
19-
<span class="flex grow font-extrabold text-contrast">{{
20-
formatMessage(messages.settingsTitle)
21-
}}</span>
22-
<div class="flex gap-2">
23-
<ButtonStyled>
24-
<nuxt-link to="/dashboard/projects"
25-
><ListIcon /> {{ formatMessage(messages.visitProjectsDashboard) }}
26-
</nuxt-link>
27-
</ButtonStyled>
12+
<nuxt-link
13+
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
14+
class="flex items-center gap-2 hover:underline hover:brightness-[--hover-brightness]"
15+
>
16+
<Avatar :src="project.icon_url" size="32px" />
17+
{{ project.title }}
18+
</nuxt-link>
19+
<ChevronRightIcon />
20+
<span class="flex grow font-extrabold text-contrast">{{
21+
formatMessage(messages.settingsTitle)
22+
}}</span>
23+
<div class="flex gap-2">
24+
<ButtonStyled>
25+
<nuxt-link to="/dashboard/projects"
26+
><ListIcon /> {{ formatMessage(messages.visitProjectsDashboard) }}
27+
</nuxt-link>
28+
</ButtonStyled>
29+
</div>
2830
</div>
31+
<ProjectMemberHeader
32+
v-if="currentMember && false"
33+
:project="project"
34+
:versions="versions"
35+
:current-member="currentMember"
36+
:is-settings="isSettings"
37+
:set-processing="setProcessing"
38+
:all-members="allMembers"
39+
:update-members="invalidateProject"
40+
:auth="auth"
41+
:tags="tags"
42+
/>
43+
</div>
44+
<div class="normal-page__content">
45+
<NuxtPage />
2946
</div>
30-
<ProjectMemberHeader
31-
v-if="currentMember && false"
32-
:project="project"
33-
:versions="versions"
34-
:current-member="currentMember"
35-
:is-settings="route.name.startsWith('type-id-settings')"
36-
:set-processing="setProcessing"
37-
:all-members="allMembers"
38-
:update-members="invalidateProject"
39-
:auth="auth"
40-
:tags="tags"
41-
/>
42-
</div>
43-
<div class="normal-page__content">
44-
<NuxtPage />
4547
</div>
46-
</div>
48+
</template>
4749

4850
<div v-else>
4951
<NewModal
@@ -811,7 +813,7 @@
811813
:project="project"
812814
:versions="versions"
813815
:current-member="currentMember"
814-
:is-settings="route.name.startsWith('type-id-settings')"
816+
:is-settings="isSettings"
815817
:route-name="route.name"
816818
:set-processing="setProcessing"
817819
:collapsed="collapsedChecklist"
@@ -1826,6 +1828,8 @@ const { data: organizationRaw } = useQuery({
18261828
// Return null when the project no longer belongs to an organization.
18271829
const organization = computed(() => (projectRaw.value?.organization ? organizationRaw.value : null))
18281830
1831+
const isSettings = computed(() => route.name.startsWith('type-id-settings'))
1832+
18291833
// Transform versionsV3 to be same shape as versionsV2 for compatibility in project pages
18301834
const versionsRaw = computed(() => {
18311835
return (versionsV3.value ?? []).map((v) => {
@@ -2262,11 +2266,27 @@ const currentMember = computed(() => {
22622266
return val
22632267
})
22642268
2269+
const canAccessSettings = computed(() => !!currentMember.value?.accepted)
2270+
22652271
const hasEditDetailsPermission = computed(() => {
22662272
const EDIT_DETAILS = 1 << 2
22672273
return (currentMember.value?.permissions & EDIT_DETAILS) === EDIT_DETAILS
22682274
})
22692275
2276+
watch(
2277+
[isSettings, currentMember],
2278+
() => {
2279+
if (isSettings.value && !canAccessSettings.value) {
2280+
showError({
2281+
fatal: true,
2282+
statusCode: 401,
2283+
statusMessage: 'Unauthorized',
2284+
})
2285+
}
2286+
},
2287+
{ flush: 'sync', immediate: true },
2288+
)
2289+
22702290
const projectTypeDisplay = computed(() => {
22712291
if (!project.value) return ''
22722292
return formatProjectType(

apps/frontend/src/pages/[type]/[id]/moderation.vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div>
2+
<div v-if="canAccess">
33
<section class="universal-card">
44
<h2>Project status</h2>
55
<Badge :type="project.status" />
@@ -107,7 +107,7 @@ import {
107107
injectProjectPageContext,
108108
} from '@modrinth/ui'
109109
import { useQuery, useQueryClient } from '@tanstack/vue-query'
110-
import { computed } from 'vue'
110+
import { computed, watch } from 'vue'
111111
112112
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
113113
import {
@@ -122,6 +122,22 @@ import {
122122
const { addNotification } = injectNotificationManager()
123123
const { projectV2: project, currentMember, invalidate } = injectProjectPageContext()
124124
125+
const canAccess = computed(() => !!currentMember.value)
126+
127+
watch(
128+
[currentMember, project],
129+
() => {
130+
if (project.value && !canAccess.value) {
131+
showError({
132+
fatal: true,
133+
statusCode: 401,
134+
statusMessage: 'Unauthorized',
135+
})
136+
}
137+
},
138+
{ flush: 'sync', immediate: true },
139+
)
140+
125141
const auth = await useAuth()
126142
const client = injectModrinthClient()
127143
const queryClient = useQueryClient()

apps/frontend/src/pages/admin.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<script setup lang="ts">
2+
definePageMeta({
3+
middleware: ['auth', 'staff'],
4+
})
5+
26
useSeoMeta({
37
robots: 'noindex',
48
})

apps/frontend/src/pages/moderation.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { FolderIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
1919
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
2020
2121
definePageMeta({
22-
middleware: 'auth',
22+
middleware: ['auth', 'staff'],
2323
})
2424
2525
useSeoMeta({

0 commit comments

Comments
 (0)