This commit is contained in:
2025-05-09 14:01:19 +02:00
parent 69f3c2bc4b
commit 9c36d1801d
6 changed files with 188 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
import { ReleasesClient } from '@/app/(home)/admin/releases/releases-client'
import { auth } from '@/server/auth'
import { HydrateClient, api } from '@/trpc/server'
import { Suspense } from 'react'
export default async function ReleasesPage() {
const session = await auth()
const isAdmin = session?.user.role === 'admin'
console.log(session)
if (!isAdmin) {
return (
<div className={'container mx-auto pt-8'}>
<div className={'prose'}>
<h1>Forbidden</h1>
</div>
</div>
)
}
await api.releases.getReleases.prefetch()
return (
<Suspense>
<HydrateClient>
<div className={'container mx-auto pt-8'}>
<ReleasesClient />
</div>
</HydrateClient>
</Suspense>
)
}

View File

@@ -0,0 +1,90 @@
'use client'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { api } from '@/trpc/react'
import { useForm } from 'react-hook-form'
export function ReleasesClient() {
const [releases] = api.releases.getReleases.useSuspenseQuery()
const addRelease = api.releases.addRelease.useMutation()
const form = useForm({
defaultValues: {
name: '',
version: '',
description: '',
url: '',
},
})
return (
<div>
<h1 className={'text-2xl'}>Releases</h1>
<div className={'mt-4'}>
<Table className={'w-full table-auto'}>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Version</TableHead>
<TableHead>Description</TableHead>
<TableHead>URL</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{releases.map((release) => (
<TableRow key={release.id}>
<TableCell>{release.name}</TableCell>
<TableCell>{release.version}</TableCell>
<TableCell>{release.description}</TableCell>
<TableCell>
<a
href={release.url}
target={'_blank'}
rel={'noopener noreferrer'}
className={'hover:underline'}
>
{release.url}
</a>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<Card className={'mt-8 max-w-xl'}>
<CardContent>
<form
className={'space-y-4'}
onSubmit={form.handleSubmit((values) => addRelease.mutate(values))}
>
<div className={'grid grid-cols-1 gap-2'}>
<Label>Title</Label>
<Input {...form.register('name')} />
</div>
<div className={'grid grid-cols-1 gap-2'}>
<Label>Version</Label>
<Input {...form.register('version')} />
</div>
<div className={'grid grid-cols-1 gap-2'}>
<Label>Description</Label>
<Input {...form.register('description')} />
</div>
<div className={'grid grid-cols-1 gap-2'}>
<Label>URL</Label>
<Input {...form.register('url')} />
</div>
<Button type={'submit'}>Add new release</Button>
</form>
</CardContent>
</Card>
</div>
)
}

View File

@@ -2,6 +2,7 @@ import { discord_router } from '@/server/api/routers/discord'
import { history_router } from '@/server/api/routers/history' import { history_router } from '@/server/api/routers/history'
import { leaderboard_router } from '@/server/api/routers/leaderboard' import { leaderboard_router } from '@/server/api/routers/leaderboard'
import { playerStateRouter } from '@/server/api/routers/player-state' import { playerStateRouter } from '@/server/api/routers/player-state'
import { releasesRouter } from '@/server/api/routers/releases'
import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc' import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc'
/** /**
@@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({
discord: discord_router, discord: discord_router,
leaderboard: leaderboard_router, leaderboard: leaderboard_router,
playerState: playerStateRouter, playerState: playerStateRouter,
releases: releasesRouter,
}) })
// export type definition of API // export type definition of API

View File

@@ -0,0 +1,38 @@
import {
adminProcedure,
createTRPCRouter,
publicProcedure,
} from '@/server/api/trpc'
import { db } from '@/server/db'
import { releases } from '@/server/db/schema'
import { z } from 'zod'
export const releasesRouter = createTRPCRouter({
getReleases: publicProcedure.query(async () => {
const res = await db.select().from(releases)
console.log(res)
return res
}),
addRelease: adminProcedure
.input(
z.object({
version: z.string(),
url: z.string(),
name: z.string(),
description: z.string(),
})
)
.mutation(async ({ input }) => {
const res = await db
.insert(releases)
.values({
version: input.version,
url: input.url,
name: input.name,
description: input.description,
})
.returning()
return res[0]
}),
})

View File

@@ -131,3 +131,17 @@ export const protectedProcedure = t.procedure
}, },
}) })
}) })
export const adminProcedure = t.procedure
.use(timingMiddleware)
.use(({ ctx, next }) => {
if (!ctx.session?.user || ctx.session.user.role !== 'admin') {
throw new TRPCError({ code: 'FORBIDDEN' })
}
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
})
})

View File

@@ -124,3 +124,16 @@ export const verificationTokens = pgTable(
}), }),
(t) => [primaryKey({ columns: [t.identifier, t.token] })] (t) => [primaryKey({ columns: [t.identifier, t.token] })]
) )
export const releases = pgTable('mod_release', {
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
name: text('name').notNull(),
description: text('description'),
version: text('version').notNull(),
url: text('url').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at')
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
})