Add team member and hunt member management for admins
This commit is contained in:
@@ -4,11 +4,12 @@
|
|||||||
- [x] Default random name
|
- [x] Default random name
|
||||||
- [ ] Change display name
|
- [ ] Change display name
|
||||||
- **Hunt Admin**
|
- **Hunt Admin**
|
||||||
- [ ] Start/Stop
|
- [x] Start/Stop
|
||||||
- [ ] Add Reviews/Score
|
- [ ] Add Reviews/Score
|
||||||
- [ ] View best Answers and Pictures
|
- [x] View best Answers and Pictures
|
||||||
- [ ] All/Best Images gallery
|
- [ ] All/Best Images gallery
|
||||||
- [ ] Manage teams (remove, kick, add)
|
- [x] Manage teams (remove, kick, add)
|
||||||
|
- [x] Manage admins (kick, add)
|
||||||
- **Hunt User**
|
- **Hunt User**
|
||||||
- [x] Create Team
|
- [x] Create Team
|
||||||
- [x] Invite Members
|
- [x] Invite Members
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
- [x] Change/Update Answer and image
|
- [x] Change/Update Answer and image
|
||||||
- [x] View Answers of other members, check for collisions!
|
- [x] View Answers of other members, check for collisions!
|
||||||
- [x] View own reviews and score
|
- [x] View own reviews and score
|
||||||
|
- [ ] Mark submission as final
|
||||||
- **Nice to have**
|
- **Nice to have**
|
||||||
- **Auth**
|
- **Auth**
|
||||||
- [ ] Login with LDAP
|
- [ ] Login with LDAP
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
- [ ] Review other answers (Fan-Favorite)
|
- [ ] Review other answers (Fan-Favorite)
|
||||||
- [ ] View all images on one page
|
- [ ] View all images on one page
|
||||||
- [ ] Notifications
|
- [ ] Notifications
|
||||||
- [ ] i18n
|
- [ ] i18n data
|
||||||
- [ ] Process image uploads (resize, remove EXIF, etc.)
|
- [ ] Process image uploads (resize, remove EXIF, etc.)
|
||||||
- [ ] Mark image as private
|
- [ ] Mark image as private
|
||||||
- [ ] Support chat
|
- [ ] Support chat
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Team } from '~/pages/hunt/[huntSlug]/teams.vue';
|
||||||
|
|
||||||
|
const { confirm } = useModal();
|
||||||
|
const { init } = useToast();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
team: Team;
|
||||||
|
huntId: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
refresh: [];
|
||||||
|
}>();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const search = ref('');
|
||||||
|
const safeSearch = ref('');
|
||||||
|
const newUser = ref<User>();
|
||||||
|
watchDebounced(
|
||||||
|
search,
|
||||||
|
(newSearch) => {
|
||||||
|
if (newSearch && newSearch.length >= 3) {
|
||||||
|
safeSearch.value = newSearch;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ debounce: 500, maxWait: 1000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
const { data: users, pending: searchPending } = await useFetch(
|
||||||
|
() => `/api/admin/hunt/${props.huntId}/teamless?email=${safeSearch.value}`,
|
||||||
|
{
|
||||||
|
immediate: false,
|
||||||
|
lazy: true,
|
||||||
|
default: () => [] as User[]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function kick(member: Team['members'][number]) {
|
||||||
|
const ok = await confirm({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message: `Are you sure you want to kick ${member.member.name} (${member.member.email}) from this team?`,
|
||||||
|
okText: 'Yes'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { success } = await $fetch(`/api/admin/team/${props.team.id}/kick`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { userId: member.member.id }
|
||||||
|
});
|
||||||
|
init({
|
||||||
|
message: success ? 'Successfully kicked user!' : 'Error while kicking',
|
||||||
|
color: success ? 'success' : 'danger'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
init({
|
||||||
|
title: 'Error while kicking',
|
||||||
|
color: 'danger',
|
||||||
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('refresh');
|
||||||
|
// await refresh();
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addUser() {
|
||||||
|
if (!newUser.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { success } = await $fetch(`/api/admin/team/${props.team.id}/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { userId: newUser.value.id }
|
||||||
|
});
|
||||||
|
init({
|
||||||
|
message: success ? 'Successfully added user!' : 'Error while adding',
|
||||||
|
color: success ? 'success' : 'danger'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
init({
|
||||||
|
title: 'Error while adding',
|
||||||
|
color: 'danger',
|
||||||
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('refresh');
|
||||||
|
newUser.value = undefined;
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTeam() {
|
||||||
|
const ok = await confirm({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message:
|
||||||
|
`Are you sure you want to delete this team?` +
|
||||||
|
(props.team._count.answers > 0
|
||||||
|
? ` This team has submitted answers!`
|
||||||
|
: ''),
|
||||||
|
okText: 'Yes'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { success } = await $fetch(`/api/admin/team/${props.team.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
init({
|
||||||
|
message: success ? 'Successfully deleted team!' : 'Error while deleting',
|
||||||
|
color: success ? 'success' : 'danger'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
init({
|
||||||
|
title: 'Error while deleting',
|
||||||
|
color: 'danger',
|
||||||
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('refresh');
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VaCollapse
|
||||||
|
:key="team.id"
|
||||||
|
:header="`${team.name} — ${team.members.length} members`"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h2 class="text-xl">
|
||||||
|
{{ team.name }}
|
||||||
|
<VaButton
|
||||||
|
color="danger"
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="loading"
|
||||||
|
@click="deleteTeam"
|
||||||
|
>
|
||||||
|
Delete team</VaButton
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Created at:
|
||||||
|
{{
|
||||||
|
$d(team.createdAt, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'medium'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
by {{ team.owner.name }} ({{ team.owner.email }})
|
||||||
|
</p>
|
||||||
|
<p>{{ team._count.answers }} answers</p>
|
||||||
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5">
|
||||||
|
<VaButton
|
||||||
|
color="danger"
|
||||||
|
v-for="member in team.members"
|
||||||
|
:key="member.member.id"
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="loading"
|
||||||
|
@click="kick(member)"
|
||||||
|
>Kick {{ member.member.name }} ({{ member.member.email }})</VaButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 md:flex-row">
|
||||||
|
<VaSelect
|
||||||
|
label="New Member"
|
||||||
|
placeholder="Start typing their email..."
|
||||||
|
autocomplete
|
||||||
|
highlight-matched-text
|
||||||
|
:options="users"
|
||||||
|
v-model="newUser"
|
||||||
|
v-model:search="search"
|
||||||
|
:loading="searchPending"
|
||||||
|
track-by="id"
|
||||||
|
text-by="email"
|
||||||
|
/><VaButton
|
||||||
|
icon="add"
|
||||||
|
color="success"
|
||||||
|
:disabled="!newUser || loading"
|
||||||
|
:loading="loading"
|
||||||
|
@click="addUser"
|
||||||
|
>Add user</VaButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VaCollapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
+1
-1
@@ -124,7 +124,7 @@
|
|||||||
"movePaginationRight": "move pagination right",
|
"movePaginationRight": "move pagination right",
|
||||||
"next": "Nächstes",
|
"next": "Nächstes",
|
||||||
"nextPeriod": "next period",
|
"nextPeriod": "next period",
|
||||||
"noOptions": "Items not found",
|
"noOptions": "Keine Elemente gefunden",
|
||||||
"noSelectedOption": "Option is not selected",
|
"noSelectedOption": "Option is not selected",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"openColorPicker": "open color picker",
|
"openColorPicker": "open color picker",
|
||||||
|
|||||||
@@ -78,6 +78,21 @@ const slugs = computed(() => checkSlug(route.params));
|
|||||||
<VaSidebarItemTitle>Hunt Admin</VaSidebarItemTitle>
|
<VaSidebarItemTitle>Hunt Admin</VaSidebarItemTitle>
|
||||||
</VaSidebarItemContent>
|
</VaSidebarItemContent>
|
||||||
</VaSidebarItem>
|
</VaSidebarItem>
|
||||||
|
<VaSidebarItem
|
||||||
|
v-if="slugs?.huntSlug && user?.role === 'ADMIN'"
|
||||||
|
text-color="danger"
|
||||||
|
hover-color="danger"
|
||||||
|
active-color="onDanger"
|
||||||
|
:to="$localePath(`/hunt/${sluggy(slugs.huntSlug)}/teams`)"
|
||||||
|
:active="
|
||||||
|
$localePath(`/hunt/${sluggy(slugs.huntSlug)}/teams`) === route.path
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<VaSidebarItemContent>
|
||||||
|
<VaIcon name="groups" />
|
||||||
|
<VaSidebarItemTitle>Hunt Teams</VaSidebarItemTitle>
|
||||||
|
</VaSidebarItemContent>
|
||||||
|
</VaSidebarItem>
|
||||||
<VaSidebarItem
|
<VaSidebarItem
|
||||||
v-if="slugs?.huntSlug && slugs?.questSlug"
|
v-if="slugs?.huntSlug && slugs?.questSlug"
|
||||||
:to="
|
:to="
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const localePath = useLocalePath();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { confirm } = useModal();
|
||||||
|
const { init } = useToast();
|
||||||
|
|
||||||
const { name, id } = app.$slugData.huntSlug!;
|
const { name, id } = app.$slugData.huntSlug!;
|
||||||
const titleStore = useTitleStore();
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
@@ -44,6 +47,35 @@ watch(loggedIn, (isLoggedIn) => {
|
|||||||
|
|
||||||
titleStore.title = data.value?.name;
|
titleStore.title = data.value?.name;
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const search = ref('');
|
||||||
|
const safeSearch = ref('');
|
||||||
|
const newUser = ref<User>();
|
||||||
|
watchDebounced(
|
||||||
|
search,
|
||||||
|
(newSearch) => {
|
||||||
|
if (newSearch && newSearch.length >= 3) {
|
||||||
|
safeSearch.value = newSearch;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ debounce: 500, maxWait: 1000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
const { data: users, pending: searchPending } = await useFetch(
|
||||||
|
() => `/api/admin/hunt/${id}/teamless?email=${safeSearch.value}`,
|
||||||
|
{
|
||||||
|
immediate: false,
|
||||||
|
lazy: true,
|
||||||
|
default: () => [] as User[]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
await $fetch(`/api/admin/hunt/${data.value?.id || id}`, {
|
await $fetch(`/api/admin/hunt/${data.value?.id || id}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -52,6 +84,72 @@ async function submit() {
|
|||||||
await refresh();
|
await refresh();
|
||||||
formData.sync();
|
formData.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function kick(member: {
|
||||||
|
member: { id: number; name: string; email: string };
|
||||||
|
}) {
|
||||||
|
const ok = await confirm({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message: `Are you sure you want to kick ${member.member.name} (${member.member.email}) from this hunt?`,
|
||||||
|
okText: 'Yes'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { success } = await $fetch(`/api/admin/hunt/${id}/member`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: { userId: member.member.id }
|
||||||
|
});
|
||||||
|
init({
|
||||||
|
message: success ? 'Successfully kicked member!' : 'Error while kicking',
|
||||||
|
color: success ? 'success' : 'danger'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
init({
|
||||||
|
title: 'Error while kicking',
|
||||||
|
color: 'danger',
|
||||||
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addUser() {
|
||||||
|
if (!newUser.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { success } = await $fetch(`/api/admin/hunt/${id}/member`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { userId: newUser.value.id }
|
||||||
|
});
|
||||||
|
init({
|
||||||
|
message: success
|
||||||
|
? 'Successfully added admin user!'
|
||||||
|
: 'Error while adding',
|
||||||
|
color: success ? 'success' : 'danger'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
init({
|
||||||
|
title: 'Error while adding',
|
||||||
|
color: 'danger',
|
||||||
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
newUser.value = undefined;
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -150,6 +248,45 @@ async function submit() {
|
|||||||
>
|
>
|
||||||
</VaButtonGroup>
|
</VaButtonGroup>
|
||||||
</VaForm>
|
</VaForm>
|
||||||
|
|
||||||
|
<VaDivider />
|
||||||
|
<h2 class="text-2xl">Hunt admin members:</h2>
|
||||||
|
<div
|
||||||
|
class="mt-8 grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5"
|
||||||
|
v-if="data"
|
||||||
|
>
|
||||||
|
<VaButton
|
||||||
|
color="danger"
|
||||||
|
v-for="member in data.members"
|
||||||
|
:key="member.member.id"
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="loading"
|
||||||
|
@click="kick(member)"
|
||||||
|
>Kick {{ member.member.name }} ({{ member.member.email }})</VaButton
|
||||||
|
>
|
||||||
|
<VaChip v-if="data.members.length <= 0">No members yet</VaChip>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 flex flex-col gap-2 md:flex-row" v-if="data">
|
||||||
|
<VaSelect
|
||||||
|
label="New Admin Member"
|
||||||
|
placeholder="Start typing their email..."
|
||||||
|
autocomplete
|
||||||
|
highlight-matched-text
|
||||||
|
:options="users"
|
||||||
|
v-model="newUser"
|
||||||
|
v-model:search="search"
|
||||||
|
:loading="searchPending"
|
||||||
|
track-by="id"
|
||||||
|
text-by="email"
|
||||||
|
/><VaButton
|
||||||
|
icon="add"
|
||||||
|
color="danger"
|
||||||
|
:disabled="!newUser || loading"
|
||||||
|
:loading="loading"
|
||||||
|
@click="addUser"
|
||||||
|
>Add admin</VaButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -43,12 +43,26 @@ async function invite() {
|
|||||||
<template>
|
<template>
|
||||||
<h1 class="text-2xl">
|
<h1 class="text-2xl">
|
||||||
{{ data?.name || name }}
|
{{ data?.name || name }}
|
||||||
|
<VaButtonGroup v-if="data?.isMember">
|
||||||
<VaButton
|
<VaButton
|
||||||
color="danger"
|
color="danger"
|
||||||
v-if="data?.isMember"
|
icon="edit"
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/admin`)"
|
:to="$localePath(`/hunt/${sluggy(data)}/admin`)"
|
||||||
>Admin</VaButton
|
>Admin</VaButton
|
||||||
|
><VaButton
|
||||||
|
color="danger"
|
||||||
|
icon="groups"
|
||||||
|
:to="$localePath(`/hunt/${sluggy(data)}/teams`)"
|
||||||
|
>Teams</VaButton
|
||||||
>
|
>
|
||||||
|
<VaButton
|
||||||
|
color="success"
|
||||||
|
icon="leaderboard"
|
||||||
|
:to="$localePath(`/hunt/${sluggy(data)}/showcase`)"
|
||||||
|
>Showcase</VaButton
|
||||||
|
>
|
||||||
|
</VaButtonGroup>
|
||||||
|
|
||||||
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
||||||
>Total points: {{ data!!.totalScore }}</VaChip
|
>Total points: {{ data!!.totalScore }}</VaChip
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Team from '~/components/admin/Team.vue';
|
||||||
|
import { useTitleStore } from '~/stores/title';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['admin']
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = useNuxtApp();
|
||||||
|
const { loggedIn } = useUserSession();
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { name, id } = app.$slugData.huntSlug!;
|
||||||
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
|
const { data, pending, refresh } = await useFetch(
|
||||||
|
`/api/admin/hunt/${id}/teams`
|
||||||
|
);
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: t('layouts.title', {
|
||||||
|
title: data.value?.name || name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(loggedIn, (isLoggedIn) => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
router.push(localePath('/login'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
titleStore.title = data.value?.name;
|
||||||
|
|
||||||
|
type Data = NonNullable<(typeof data)['value']>;
|
||||||
|
export type Team = Data['teams'][number];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>teams</h1>
|
||||||
|
<VaButton color="success" icon="replay" @click="refresh">Refresh</VaButton>
|
||||||
|
<VaAccordion v-if="data" stateful>
|
||||||
|
<Team
|
||||||
|
v-for="team in data.teams"
|
||||||
|
:key="team.id"
|
||||||
|
:team="team"
|
||||||
|
:huntId="id"
|
||||||
|
@refresh="refresh"
|
||||||
|
/>
|
||||||
|
</VaAccordion>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -55,7 +55,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
select: {
|
select: {
|
||||||
createdAt: true,
|
|
||||||
member: {
|
member: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
|
});
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user