diff --git a/TODO.md b/TODO.md index 4d29c06..2b93361 100644 --- a/TODO.md +++ b/TODO.md @@ -4,11 +4,12 @@ - [x] Default random name - [ ] Change display name - **Hunt Admin** - - [ ] Start/Stop + - [x] Start/Stop - [ ] Add Reviews/Score - - [ ] View best Answers and Pictures + - [x] View best Answers and Pictures - [ ] All/Best Images gallery - - [ ] Manage teams (remove, kick, add) + - [x] Manage teams (remove, kick, add) + - [x] Manage admins (kick, add) - **Hunt User** - [x] Create Team - [x] Invite Members @@ -17,6 +18,7 @@ - [x] Change/Update Answer and image - [x] View Answers of other members, check for collisions! - [x] View own reviews and score + - [ ] Mark submission as final - **Nice to have** - **Auth** - [ ] Login with LDAP @@ -36,7 +38,7 @@ - [ ] Review other answers (Fan-Favorite) - [ ] View all images on one page - [ ] Notifications - - [ ] i18n + - [ ] i18n data - [ ] Process image uploads (resize, remove EXIF, etc.) - [ ] Mark image as private - [ ] Support chat diff --git a/app/components/admin/Team.vue b/app/components/admin/Team.vue new file mode 100644 index 0000000..7675d7a --- /dev/null +++ b/app/components/admin/Team.vue @@ -0,0 +1,210 @@ + + + + + + + {{ team.name }} + + Delete team + + + Created at: + {{ + $d(team.createdAt, { + dateStyle: 'medium', + timeStyle: 'medium' + }) + }} + by {{ team.owner.name }} ({{ team.owner.email }}) + + {{ team._count.answers }} answers + + Kick {{ member.member.name }} ({{ member.member.email }}) + + + Add user + + + + + + diff --git a/app/lang/de.json b/app/lang/de.json index 3be0c85..1a48fcc 100644 --- a/app/lang/de.json +++ b/app/lang/de.json @@ -124,7 +124,7 @@ "movePaginationRight": "move pagination right", "next": "Nächstes", "nextPeriod": "next period", - "noOptions": "Items not found", + "noOptions": "Keine Elemente gefunden", "noSelectedOption": "Option is not selected", "ok": "OK", "openColorPicker": "open color picker", diff --git a/app/layouts/default.vue b/app/layouts/default.vue index c345013..fc9f92f 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -78,6 +78,21 @@ const slugs = computed(() => checkSlug(route.params)); Hunt Admin + + + + Hunt Teams + + Hunt admin members: + + Kick {{ member.member.name }} ({{ member.member.email }}) + No members yet + + + Add admin + diff --git a/app/pages/hunt/[huntSlug]/index.vue b/app/pages/hunt/[huntSlug]/index.vue index bca3c3d..96dc4e0 100644 --- a/app/pages/hunt/[huntSlug]/index.vue +++ b/app/pages/hunt/[huntSlug]/index.vue @@ -43,12 +43,26 @@ async function invite() { {{ data?.name || name }} - Admin + + AdminTeams + Showcase + + Total points: {{ data!!.totalScore }} diff --git a/app/pages/hunt/[huntSlug]/teams.vue b/app/pages/hunt/[huntSlug]/teams.vue new file mode 100644 index 0000000..99beb59 --- /dev/null +++ b/app/pages/hunt/[huntSlug]/teams.vue @@ -0,0 +1,54 @@ + + + + teams + Refresh + + + + + + diff --git a/server/api/admin/hunt/[huntId]/index.get.ts b/server/api/admin/hunt/[huntId]/index.get.ts index bf434b4..005bed5 100644 --- a/server/api/admin/hunt/[huntId]/index.get.ts +++ b/server/api/admin/hunt/[huntId]/index.get.ts @@ -55,7 +55,6 @@ export default defineEventHandler(async (event) => { }, members: { select: { - createdAt: true, member: { select: { id: true, diff --git a/server/api/admin/hunt/[huntId]/member.delete.ts b/server/api/admin/hunt/[huntId]/member.delete.ts new file mode 100644 index 0000000..72858e8 --- /dev/null +++ b/server/api/admin/hunt/[huntId]/member.delete.ts @@ -0,0 +1,60 @@ +import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event); + + if (user.user.role !== 'ADMIN') { + throw createError({ + status: 403, + statusText: 'Not allowed!' + }); + } + + const { huntId } = await useValidatedParams( + event, + z.object({ + huntId: zh.numAsString + }) + ); + + const { userId } = await useValidatedBody( + event, + z.object({ + userId: z.int().gt(0) + }) + ); + + try { + await prisma.huntMember.delete({ + where: { + memberId_huntId: { + memberId: userId, + huntId + }, + hunt: { + OR: [ + { + creatorId: user.user.id + }, + { + members: { + some: { + memberId: user.user.id + } + } + } + ] + } + }, + select: { + memberId: true + } + }); + } catch (e) { + console.error('Kick member from hunt', huntId, userId, user.user.id, e); + throw createError({ status: 404, statusText: 'Not found!', cause: e }); + } + + return { success: true }; +}); diff --git a/server/api/admin/hunt/[huntId]/member.post.ts b/server/api/admin/hunt/[huntId]/member.post.ts new file mode 100644 index 0000000..561f415 --- /dev/null +++ b/server/api/admin/hunt/[huntId]/member.post.ts @@ -0,0 +1,116 @@ +import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event); + + if (user.user.role !== 'ADMIN') { + throw createError({ + status: 403, + statusText: 'Not allowed!' + }); + } + + const { huntId } = await useValidatedParams( + event, + z.object({ + huntId: zh.numAsString + }) + ); + + const { userId } = await useValidatedBody( + event, + z.object({ + userId: z.int().gt(0) + }) + ); + + const hunt = await prisma.hunt.findUnique({ + where: { + id: huntId, + OR: [ + { + creatorId: user.user.id + }, + { + members: { + some: { + memberId: user.user.id + } + } + } + ] + }, + select: { + id: true, + members: { + select: { + memberId: true + }, + where: { + memberId: userId + }, + take: 1 + } + } + }); + + if (!hunt) { + throw createError({ status: 404, statusText: 'Not found!' }); + } + + const newUser = await prisma.user.findUnique({ + select: { + id: true, + role: true + }, + where: { + id: userId, + teamsOwner: { + none: { + huntId + } + }, + teamsMember: { + none: { + team: { + huntId + } + } + }, + huntsMember: { + none: { + huntId + } + }, + huntsCreated: { + none: { + id: huntId + } + } + } + }); + + if (!newUser) { + throw createError({ status: 409, statusText: 'User is already member!' }); + } + + await prisma.huntMember.create({ + data: { + huntId: hunt.id, + memberId: newUser.id + } + }); + if (newUser.role !== 'ADMIN') { + await prisma.user.update({ + where: { + id: newUser.id + }, + data: { + role: 'ADMIN' + } + }); + } + + return { success: true }; +}); diff --git a/server/api/admin/hunt/[huntId]/teamless.get.ts b/server/api/admin/hunt/[huntId]/teamless.get.ts new file mode 100644 index 0000000..04f9d10 --- /dev/null +++ b/server/api/admin/hunt/[huntId]/teamless.get.ts @@ -0,0 +1,88 @@ +import { useValidatedParams, useValidatedQuery, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event); + + if (user.user.role !== 'ADMIN') { + throw createError({ + status: 403, + statusText: 'Not allowed!' + }); + } + const userId = user.user.id; + const { huntId } = await useValidatedParams( + event, + z.object({ + huntId: zh.numAsString + }) + ); + const hunt = await prisma.hunt.findUnique({ + where: { + id: huntId, + OR: [ + { + creatorId: userId + }, + { + members: { + some: { + memberId: userId + } + } + } + ] + }, + select: { id: true } + }); + + if (!hunt) { + throw createError({ status: 404, statusText: 'Not found!' }); + } + + const { email } = await useValidatedQuery( + event, + z.object({ + email: z.string().min(3).trim() + }) + ); + + return prisma.user.findMany({ + select: { + email: true, + name: true, + id: true + }, + where: { + email: { + contains: email + }, + teamsOwner: { + none: { + huntId + } + }, + teamsMember: { + none: { + team: { + huntId + } + } + }, + huntsMember: { + none: { + huntId + } + }, + huntsCreated: { + none: { + id: huntId + } + } + }, + take: 10, + orderBy: { + createdAt: 'desc' + } + }); +}); diff --git a/server/api/admin/hunt/[huntId]/teams.get.ts b/server/api/admin/hunt/[huntId]/teams.get.ts new file mode 100644 index 0000000..b491a76 --- /dev/null +++ b/server/api/admin/hunt/[huntId]/teams.get.ts @@ -0,0 +1,83 @@ +import { useValidatedParams, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event); + + if (user.user.role !== 'ADMIN') { + throw createError({ + status: 403, + statusText: 'Not allowed!' + }); + } + const userId = user.user.id; + const { huntId } = await useValidatedParams( + event, + z.object({ + huntId: zh.numAsString + }) + ); + + const hunt = await prisma.hunt.findUnique({ + where: { + id: huntId, + OR: [ + { + creatorId: userId + }, + { + members: { + some: { + memberId: userId + } + } + } + ] + }, + select: { + id: true, + name: true, + teams: { + omit: { + huntId: true, + ownerId: true + }, + orderBy: { + createdAt: 'desc' + }, + include: { + owner: { + select: { + id: true, + name: true, + email: true + } + }, + _count: { + select: { + answers: true + } + }, + members: { + select: { + createdAt: true, + member: { + select: { + id: true, + name: true, + email: true + } + } + } + } + } + } + } + }); + + if (!hunt) { + throw createError({ status: 404, statusText: 'Not found!' }); + } + + return hunt; +}); diff --git a/server/api/admin/team/[teamId]/add.post.ts b/server/api/admin/team/[teamId]/add.post.ts new file mode 100644 index 0000000..278f98d --- /dev/null +++ b/server/api/admin/team/[teamId]/add.post.ts @@ -0,0 +1,110 @@ +import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event); + + if (user.user.role !== 'ADMIN') { + throw createError({ + status: 403, + statusText: 'Not allowed!' + }); + } + + const { teamId } = await useValidatedParams( + event, + z.object({ + teamId: zh.numAsString + }) + ); + + const { userId } = await useValidatedBody( + event, + z.object({ + userId: z.int().gt(0) + }) + ); + + const team = await prisma.huntTeam.findUnique({ + where: { + id: teamId, + hunt: { + OR: [ + { + creatorId: user.user.id + }, + { + members: { + some: { + memberId: user.user.id + } + } + } + ] + } + }, + select: { + id: true, + ownerId: true, + huntId: true, + members: { + select: { + memberId: true + }, + where: { + memberId: userId + }, + take: 1 + } + } + }); + + if (!team) { + throw createError({ status: 404, statusText: 'Not found!' }); + } + const huntId = team.huntId; + + const newUser = await prisma.user.findUnique({ + select: { + id: true + }, + where: { + id: userId, + teamsOwner: { + none: { + huntId + } + }, + teamsMember: { + none: { + team: { + huntId + } + } + }, + huntsMember: { + none: { + huntId + } + }, + huntsCreated: { + none: { + id: huntId + } + } + } + }); + + if (!newUser) { + throw createError({ status: 409, statusText: 'User is already member!' }); + } + + await prisma.teamMember.create({ + data: { + teamId: team.id, + memberId: newUser.id + } + }); + + return { success: true }; +}); diff --git a/server/api/admin/team/[teamId]/index.delete.ts b/server/api/admin/team/[teamId]/index.delete.ts new file mode 100644 index 0000000..c60f172 --- /dev/null +++ b/server/api/admin/team/[teamId]/index.delete.ts @@ -0,0 +1,47 @@ +import { useValidatedParams, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event); + + if (user.user.role !== 'ADMIN') { + throw createError({ + status: 403, + statusText: 'Not allowed!' + }); + } + + const { teamId } = await useValidatedParams( + event, + z.object({ + teamId: zh.numAsString + }) + ); + + try { + await prisma.huntTeam.delete({ + where: { + id: teamId, + hunt: { + OR: [ + { + creatorId: user.user.id + }, + { + members: { + some: { + memberId: user.user.id + } + } + } + ] + } + } + }); + } catch (e) { + console.error('Delete team', teamId, user.user.id, e); + throw createError({ status: 404, statusText: 'Not found!', cause: e }); + } + + return { success: true }; +}); diff --git a/server/api/admin/team/[teamId]/kick.post.ts b/server/api/admin/team/[teamId]/kick.post.ts new file mode 100644 index 0000000..d2297e3 --- /dev/null +++ b/server/api/admin/team/[teamId]/kick.post.ts @@ -0,0 +1,62 @@ +import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event); + + if (user.user.role !== 'ADMIN') { + throw createError({ + status: 403, + statusText: 'Not allowed!' + }); + } + + const { teamId } = await useValidatedParams( + event, + z.object({ + teamId: zh.numAsString + }) + ); + + const { userId } = await useValidatedBody( + event, + z.object({ + userId: z.int().gt(0) + }) + ); + + try { + await prisma.teamMember.delete({ + where: { + memberId_teamId: { + memberId: userId, + teamId + }, + team: { + hunt: { + OR: [ + { + creatorId: user.user.id + }, + { + members: { + some: { + memberId: user.user.id + } + } + } + ] + } + } + }, + select: { + memberId: true + } + }); + } catch (e) { + console.error('Kick user from team', teamId, userId, user.user.id, e); + throw createError({ status: 404, statusText: 'Not found!', cause: e }); + } + + return { success: true }; +});
+ Created at: + {{ + $d(team.createdAt, { + dateStyle: 'medium', + timeStyle: 'medium' + }) + }} + by {{ team.owner.name }} ({{ team.owner.email }}) +
{{ team._count.answers }} answers