Compare commits

...

11 Commits

Author SHA1 Message Date
2f6b3521cd add crown to winner team
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 27s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 13s
2025-07-02 17:55:06 +02:00
6789a65285 add death to admin ui
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 14s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 14s
2025-06-25 16:08:03 +02:00
7a0db65f78 add copy public report button
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 20s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 22s
2025-06-25 14:46:16 +02:00
94aa6ea377 fix invalid date on admin ui report create 2025-06-25 14:26:21 +02:00
a06cc34085 fix creation date not set if finished report is added via api
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 15s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 14s
2025-06-24 17:24:24 +02:00
9041578252 do not show killer name on team member hover
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 15s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 14s
2025-06-24 01:16:00 +02:00
deafb65c75 add admin tools
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 15s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 14s
2025-06-24 01:13:34 +02:00
2eb9891b3c add id to strike reasons 2025-06-24 00:39:47 +02:00
36fe39845f fix member kills in teams table
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 23s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 14s
2025-06-24 00:32:06 +02:00
d82ac4f275 fix team sorting
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 28s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 14s
2025-06-24 00:31:14 +02:00
136e0b808c fix wrong permission checks
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 22s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 13s
2025-06-24 00:29:52 +02:00
26 changed files with 532 additions and 30 deletions

View File

@ -6,6 +6,7 @@ import { team } from './team.ts';
import { settings } from './settings.ts';
import { feedback } from './feedback.ts';
import { report } from './report.ts';
import { tools } from './tools.ts';
export const server = {
admin,
@ -15,5 +16,6 @@ export const server = {
user,
report,
feedback,
settings
settings,
tools
};

View File

@ -128,5 +128,53 @@ export const team = {
teams: await db.getTeams(input)
};
}
}),
addDeath: defineAction({
input: z.object({
deadUserId: z.number(),
killerUserId: z.number().nullish(),
message: z.string()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
const { id } = await db.addDeath(input);
return {
id: id
};
}
}),
editDeath: defineAction({
input: z.object({
id: z.number(),
deadUserId: z.number(),
killerUserId: z.number().nullish(),
message: z.string()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
await db.editDeath(input);
}
}),
deleteDeath: defineAction({
input: z.object({
id: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
await db.deleteDeath(input);
}
}),
deaths: defineAction({
handler: async (_, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
return {
deaths: await db.getDeaths({})
};
}
})
};

57
src/actions/tools.ts Normal file
View File

@ -0,0 +1,57 @@
import { ActionError, defineAction } from 'astro:actions';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { z } from 'astro:schema';
import { getBedrockUuid, getJavaUuid } from '@util/minecraft.ts';
export const tools = {
uuidFromUsername: defineAction({
input: z.object({
edition: z.enum(['java', 'bedrock']),
username: z.string()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Tools);
let uuid = null;
switch (input.edition) {
case 'java':
try {
uuid = await getJavaUuid(input.username);
} catch (_) {
throw new ActionError({
code: 'NOT_FOUND',
message: `Der Username ${input.username} existiert nicht`
});
}
if (uuid == null) {
throw new ActionError({
code: 'BAD_REQUEST',
message: `Während der Anfrage zur Mojang API ist ein Fehler aufgetreten`
});
}
break;
case 'bedrock':
try {
uuid = await getBedrockUuid(input.username);
} catch (_) {
throw new ActionError({
code: 'NOT_FOUND',
message: `Der Username ${input.username} existiert nicht`
});
}
if (uuid == null) {
throw new ActionError({
code: 'BAD_REQUEST',
message: `Während der Anfrage zum Username Resolver ist ein Fehler aufgetreten`
});
}
break;
}
return {
uuid: uuid
};
}
})
};

View File

@ -6,6 +6,7 @@
import TeamSearch from '@components/admin/search/TeamSearch.svelte';
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import Icon from '@iconify/svelte';
// html bindings
let previewDialogElem: HTMLDialogElement;
@ -76,13 +77,27 @@
}
};
}
function onCopyPublicLink(urlHash: string) {
navigator.clipboard.writeText(`${document.baseURI}report/${urlHash}`);
document.activeElement?.blur();
}
</script>
<div
class="absolute bottom-2 bg-base-200 rounded-lg w-[calc(100%-1rem)] mx-2 flex px-6 py-4 gap-2"
hidden={report === null}
>
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={() => (report = null)}>✕</button>
<div class="absolute right-2 top-2">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost"><Icon icon="heroicons:share" /></div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul tabindex="0" class="menu dropdown-content bg-base-100 rounded-box z-1 p-2 shadow-sm w-max">
<li><button onclick={() => onCopyPublicLink(report?.urlHash)}>Öffentlichen Report Link kopieren</button></li>
</ul>
</div>
<button class="btn btn-sm btn-circle btn-ghost" onclick={() => (report = null)}>✕</button>
</div>
<div class="w-[34rem]">
<TeamSearch value={report?.reporter.name} label="Report Team" readonly mustMatch />
<TeamSearch value={report?.reported?.name} label="Reportetes Team" onSubmit={(team) => (reportedTeam = team)} />

View File

@ -61,7 +61,7 @@
key: 'createdAt',
type: 'checkbox',
label: 'Report kann bearbeitet werden',
options: { convert: (v) => (v ? new Date().toISOString() : null) }
options: { convert: (v) => (v ? null : new Date().toISOString()) }
}
]
]}

View File

@ -33,7 +33,8 @@
count={true}
keys={[
{ key: 'name', label: 'Name', width: 20 },
{ key: 'weight', label: 'Gewichtung', width: 70, sortable: true }
{ key: 'weight', label: 'Gewichtung', width: 50, sortable: true },
{ key: 'id', label: 'Id', width: 20 }
]}
onDelete={onBlockedUserDelete}
onEdit={(strikeReason) => (editPopupStrikeReason = strikeReason)}

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { addDeath, fetchDeaths } from '@app/admin/teamDeaths/teamDeaths.ts';
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
// states
let createPopupOpen = $state(false);
// lifecycle
$effect(() => {
fetchDeaths();
});
</script>
<div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span>Neuer Spielertod</span>
</button>
</div>
<CrudPopup
texts={{
title: 'Spielertod erstellen',
submitButtonTitle: 'Erstellen',
confirmPopupTitle: 'Spielertod erstellen?',
confirmPopupMessage: 'Soll der neue Spielertod erstellt werden?'
}}
target={null}
keys={[
[
{
key: 'killed',
type: 'user-search',
label: 'Getöteter Spieler',
options: { required: true, validate: (user) => !!user?.id }
},
{
key: 'killer',
type: 'user-search',
label: 'Killer',
options: { validate: (user) => (user?.username ? !!user?.id : true) }
}
],
[{ key: 'message', type: 'textarea', label: 'Todesnachricht', options: { required: true, dynamicWidth: true } }]
]}
onSubmit={addDeath}
bind:open={createPopupOpen}
/>

View File

@ -0,0 +1,69 @@
<script lang="ts">
// state
import { type Death, deaths, deleteDeath, editDeath } from '@app/admin/teamDeaths/teamDeaths.ts';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import DataTable from '@components/admin/table/DataTable.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
let editPopupDeath = $state(null);
let editPopupOpen = $derived(!!editPopupDeath);
// lifecycle
$effect(() => {
if (!editPopupOpen) editPopupDeath = null;
});
// callbacks
function onDeathDelete(death: Death) {
$confirmPopupState = {
title: 'Tod löschen?',
message: 'Soll der Tod wirklich gelöscht werden?',
onConfirm: () => deleteDeath(death)
};
}
</script>
{#snippet username(user?: { id: number; username: string })}
{user?.username}
{/snippet}
<DataTable
data={deaths}
count={true}
keys={[
{ key: 'killed', label: 'Getöteter Spieler', width: 20, transform: username },
{ key: 'killer', label: 'Killer', width: 20, transform: username },
{ key: 'message', label: 'Todesnachricht', width: 50 }
]}
onEdit={(death) => (editPopupDeath = death)}
onDelete={onDeathDelete}
/>
<CrudPopup
texts={{
title: 'Tod bearbeiten',
submitButtonTitle: 'Speichern',
confirmPopupTitle: 'Änderungen speichern?',
confirmPopupMessage: 'Sollen die Änderungen gespeichert werden?'
}}
target={editPopupDeath}
keys={[
[
{
key: 'killed',
type: 'user-search',
label: 'Getöteter Spieler',
options: { required: true, validate: (user) => !!user?.id }
},
{
key: 'killer',
type: 'user-search',
label: 'Killer',
options: { validate: (user) => (user?.username ? !!user?.id : true) }
}
],
[{ key: 'message', type: 'textarea', label: 'Todesnachricht', options: { required: true, dynamicWidth: true } }]
]}
onSubmit={editDeath}
bind:open={editPopupDeath}
/>

View File

@ -0,0 +1,61 @@
import { type ActionReturnType, actions } from 'astro:actions';
import { writable } from 'svelte/store';
import { actionErrorPopup } from '@util/action.ts';
import { addToWritableArray, deleteFromWritableArray, updateWritableArray } from '@util/state.ts';
// types
export type Deaths = Exclude<ActionReturnType<typeof actions.team.deaths>['data'], undefined>['deaths'];
export type Death = Deaths[0];
// state
export const deaths = writable<Deaths>([]);
// actions
export async function fetchDeaths() {
const { data, error } = await actions.team.deaths();
if (error) {
actionErrorPopup(error);
return;
}
deaths.set(data.deaths);
}
export async function addDeath(death: Death) {
const { data, error } = await actions.team.addDeath({
deadUserId: death.killed.id,
killerUserId: death.killer?.id,
message: death.message
});
if (error) {
actionErrorPopup(error);
return;
}
addToWritableArray(deaths, Object.assign(death, { id: data.id }));
}
export async function editDeath(death: Death) {
const { error } = await actions.team.editDeath({
id: death.id,
deadUserId: death.killed.id,
killerUserId: death.killer?.id,
message: death.message
});
if (error) {
actionErrorPopup(error);
return;
}
updateWritableArray(deaths, death, (d) => d.id == death.id);
}
export async function deleteDeath(death: Death) {
const { error } = await actions.team.deleteDeath({ id: death.id });
if (error) {
actionErrorPopup(error);
return;
}
deleteFromWritableArray(deaths, (d) => d.id == death.id);
}

View File

@ -0,0 +1,29 @@
<script lang="ts">
import Input from '@components/input/Input.svelte';
import Select from '@components/input/Select.svelte';
import { uuidFromUsername } from '@app/admin/tools/tools.ts';
// states
let edition = $state<'java' | 'bedrock'>('java');
let username = $state('');
let uuid = $state<null | string>(null);
// callbacks
async function onSubmit() {
uuid = await uuidFromUsername(edition, username);
}
</script>
<fieldset class="fieldset border border-base-200 rounded-box px-4">
<legend class="fieldset-legend">Account UUID finder</legend>
<div>
<div class="flex gap-3">
<Input bind:value={username} />
<Select bind:value={edition} values={{ java: 'Java', bedrock: 'Bedrock' }} />
</div>
<div class="flex justify-center">
<button class="btn w-4/6" class:disabled={!username} onclick={onSubmit}>UUID finden</button>
</div>
<Input bind:value={uuid} readonly />
</div>
</fieldset>

View File

@ -0,0 +1,7 @@
<script lang="ts">
import AccountUuidFinder from '@app/admin/tools/AccountUuidFinder.svelte';
</script>
<div class="flex justify-center mt-2">
<AccountUuidFinder />
</div>

View File

@ -0,0 +1,12 @@
import { actions } from 'astro:actions';
import { actionErrorPopup } from '@util/action.ts';
export async function uuidFromUsername(edition: 'java' | 'bedrock', username: string) {
const { data, error } = await actions.tools.uuidFromUsername({ edition: edition, username: username });
if (error) {
actionErrorPopup(error);
return null;
}
return data.uuid;
}

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { type ActionReturnType, actions } from 'astro:actions';
import type { db } from '@db/database.ts';
import crown from '@assets/img/crown.svg';
interface Props {
teams: Exclude<ActionReturnType<typeof actions.team.teams>['data'], undefined>['teams'];
@ -35,8 +36,14 @@
return Number(!!aBothDead) - Number(!!bBothDead);
}
return bBothKills.length - aBothKills.length;
return bBothKills - aBothKills;
});
const aliveTeams = entries.reduce(
(prev, curr) =>
prev + Number(curr.memberOne.id && curr.memberTwo.id && (!curr.memberOne.dead || !curr.memberTwo.dead)),
0
);
</script>
<div class="card bg-base-300 shadow-sm w-full md:w-5/7 xl:w-4/7 sm:p-5 md:p-10">
@ -51,36 +58,48 @@
</thead>
<tbody>
{#each entries as team (team.id)}
{@const teamSignedUp = !!team.memberOne.id && !!team.memberTwo.id}
{@const teamDead = !!team.memberOne.dead && !!team.memberTwo.dead}
<tr>
<td>
<div class="flex items-center gap-x-2">
<div class="rounded-sm min-w-3 w-3 min-h-3 h-3" style="background-color: {team.color}"></div>
<h3
class="text-xs sm:text-xl break-all"
class:line-through={team.memberOne.dead && team.memberTwo.dead}
class:text-red-200={!team.memberOne}
>
<div class="relative">
<div class="rounded-sm min-w-3 w-3 min-h-3 h-3" style="background-color: {team.color}"></div>
{#if aliveTeams === 1 && teamSignedUp && !teamDead}
<div class="absolute h-3.5 w-3.5 -top-2.25 -right-0.25">
<img class="h-full w-full" src={crown.src} alt="" />
</div>
{/if}
</div>
<h3 class="text-xs sm:text-xl break-all" class:line-through={teamDead}>
{team.name}
</h3>
</div>
{#if !team.memberOne.id || !team.memberTwo.id}
{#if !teamSignedUp}
<span>Team unvollständig</span>
{/if}
</td>
<td class="max-w-9 overflow-ellipsis">
<div class="flex items-center gap-x-2 w-max tooltip">
{#if team.memberTwo.kills.length > 0 || team.memberTwo.dead}
{#if team.memberOne.kills.length > 0 || team.memberOne.dead}
<div class="tooltip-content text-left space-y-1">
{#each team.memberTwo.kills as kill (kill.killed.id)}
<p>🔪 {kill.killer!.username}{kill.killed.username}</p>
{#each team.memberOne.kills as kill (kill.killed.id)}
<p>🔪 {kill.killed.username}</p>
{/each}
{#if team.memberTwo.dead}
<p class="mt-2 first:mt-0">{team.memberTwo.dead.message}</p>
{#if team.memberOne.dead}
<p class="mt-2 first:mt-0">{team.memberOne.dead.message}</p>
{/if}
</div>
{/if}
{#if team.memberOne.id != null}
<img class="h-4 pixelated" src="https://mc-heads.net/head/{team.memberOne.username}/8" alt="head" />
<div class="relative">
<img class="h-4 pixelated" src="https://mc-heads.net/head/{team.memberOne.username}/8" alt="head" />
{#if aliveTeams === 1 && teamSignedUp && !teamDead}
<div class="absolute -top-1.25 -right-1.25">
<img class="h-3 w-3 rotate-30" src={crown.src} alt="" />
</div>
{/if}
</div>
{/if}
<span
class="text-xs sm:text-md break-all"
@ -94,7 +113,7 @@
{#if team.memberTwo.kills.length > 0 || team.memberTwo.dead}
<div class="tooltip-content text-left space-y-1">
{#each team.memberTwo.kills as kill (kill.killed.id)}
<p>🔪 {kill.killer!.username}{kill.killed.username}</p>
<p>🔪 {kill.killed.username}</p>
{/each}
{#if team.memberTwo.dead}
<p class="mt-2 first:mt-0">{team.memberTwo.dead.message}</p>
@ -102,7 +121,14 @@
</div>
{/if}
{#if team.memberTwo.id != null}
<img class="h-4 pixelated" src="https://mc-heads.net/head/{team.memberTwo.username}/8" alt="head" />
<div class="relative">
<img class="h-4 pixelated" src="https://mc-heads.net/head/{team.memberTwo.username}/8" alt="head" />
{#if aliveTeams === 1 && teamSignedUp && !teamDead}
<div class="absolute -top-1.25 -right-1.25">
<img class="h-3 w-3 rotate-30" src={crown.src} alt="" />
</div>
{/if}
</div>
{/if}
<span
class="text-xs sm:text-md break-all"

1
src/assets/img/crown.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><g fill="#f79329"><path d="m91.56 50.38l14.35 44.94l-36.36-4.71z"/><path d="M105.91 96.5c-.05 0-.1 0-.15-.01l-36.37-4.71c-.39-.05-.72-.29-.9-.64s-.17-.76.01-1.1l22.02-40.23c.23-.41.69-.65 1.15-.61c.47.04.87.36 1.01.81l14.24 44.62c.14.19.22.43.22.68c0 .65-.53 1.18-1.18 1.18c0 .01-.03.01-.05.01M71.4 89.66l32.82 4.25l-12.94-40.55zM40.19 34.91a5.46 5.46 0 0 1-5.46 5.46c-3.01 0-5.46-2.45-5.46-5.46c0-3.02 2.44-5.46 5.46-5.46s5.46 2.44 5.46 5.46"/><path d="M34.73 41.54a6.65 6.65 0 0 1-6.64-6.64a6.65 6.65 0 0 1 6.64-6.64a6.65 6.65 0 0 1 6.64 6.64a6.65 6.65 0 0 1-6.64 6.64m0-10.91c-2.36 0-4.28 1.92-4.28 4.28s1.92 4.28 4.28 4.28s4.29-1.92 4.29-4.28s-1.93-4.28-4.29-4.28m58.85-1.18c3.01.18 5.31 2.77 5.13 5.78c-.17 3.01-2.77 5.3-5.77 5.13a5.45 5.45 0 0 1-5.13-5.77c.18-3.02 2.76-5.32 5.77-5.14"/><path d="m93.26 41.54l-.39-.01c-1.77-.1-3.4-.89-4.57-2.21a6.62 6.62 0 0 1-1.67-4.8a6.647 6.647 0 0 1 6.63-6.25l.39.01c3.66.22 6.46 3.38 6.24 7.03a6.64 6.64 0 0 1-6.63 6.23m.23-10.92c-2.5 0-4.37 1.77-4.5 4.03c-.07 1.14.31 2.24 1.07 3.1s1.8 1.36 2.95 1.43l.25.01c2.26 0 4.14-1.77 4.27-4.03c.14-2.36-1.67-4.39-4.03-4.54zM36.43 50.38L22.09 95.32l36.36-4.71z"/><path d="M22.09 96.5c-.34 0-.68-.15-.91-.42c-.26-.31-.34-.73-.22-1.11L35.3 50.03c.14-.45.54-.77 1.01-.81c.51-.05.92.19 1.15.61l22.02 40.23c.18.34.19.75.01 1.1c-.17.35-.51.58-.9.64l-36.36 4.71c-.04-.01-.09-.01-.14-.01m14.63-43.14L23.77 93.92l32.82-4.25z"/></g><use href="#notoV1Crown1"/><use href="#notoV1Crown1"/><defs><path id="notoV1Crown0" d="M119.5 53.43a1.18 1.18 0 0 0-1.29.22L87.25 82.71L65.16 49.72c-.22-.33-.58-.52-.98-.52c-.39 0-.76.19-.98.51l-22.19 33l-30.95-29.07a1.18 1.18 0 0 0-1.29-.22c-.43.19-.71.63-.69 1.1l1.27 47.52c0 10.33 24.06 18.43 54.78 18.43s54.78-8.1 54.78-18.4l1.27-47.55c.02-.46-.25-.9-.68-1.09"/><path id="notoV1Crown1" fill="#fcc21b" d="M72.17 28.76c0 4.51-3.66 8.17-8.17 8.17s-8.18-3.66-8.18-8.17c0-4.52 3.66-8.17 8.18-8.17s8.17 3.65 8.17 8.17m-58.72 6.15c0 3.58-2.9 6.48-6.49 6.48c-3.58 0-6.48-2.9-6.48-6.48c0-3.59 2.9-6.49 6.48-6.49c3.59 0 6.49 2.9 6.49 6.49m101.09 0c0 3.58 2.9 6.48 6.49 6.48c3.58 0 6.49-2.9 6.49-6.48a6.49 6.49 0 0 0-6.49-6.49a6.49 6.49 0 0 0-6.49 6.49"/></defs><use fill="#fcc21b" href="#notoV1Crown0"/><clipPath id="notoV1Crown2"><use href="#notoV1Crown0"/></clipPath><path fill="#d7598b" d="m119.91 78.06l.01.01l-.59 18.85h-.01c-4.2-.13-7.46-4.45-7.3-9.66c.16-5.22 3.69-9.33 7.89-9.2m-111.54 0l-.01.01l.58 18.85h.02c4.19-.13 7.46-4.45 7.29-9.66c-.16-5.22-3.69-9.33-7.88-9.2" clip-path="url(#notoV1Crown2)"/><path fill="#d7598b" d="M72.8 96.55c0 5.58-3.88 10.11-8.67 10.11c-4.78 0-8.66-4.53-8.66-10.11c0-5.59 3.88-10.11 8.66-10.11c4.79-.01 8.67 4.52 8.67 10.11"/><g fill="#ed6c30"><path d="M89.9 102.14c-.13 2.7-2.12 4.79-4.44 4.68c-2.31-.11-4.08-2.4-3.94-5.09c.14-2.71 2.13-4.8 4.44-4.68c2.31.1 4.07 2.39 3.94 5.09"/><ellipse cx="103.04" cy="98.95" rx="4.89" ry="4.2" transform="rotate(-87.013 103.044 98.958)"/></g><g fill="#ed6c30"><path d="M38.37 102.14c.13 2.7 2.12 4.79 4.44 4.68c2.31-.11 4.08-2.4 3.94-5.09c-.13-2.71-2.12-4.8-4.43-4.68c-2.32.1-4.09 2.39-3.95 5.09"/><ellipse cx="25.23" cy="98.95" rx="4.19" ry="4.89" transform="rotate(-2.987 25.234 98.957)"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -111,11 +111,11 @@
submitEnabled = false;
for (const key of keys) {
for (const k of key) {
if (k.options?.required) {
if (k.options?.validate) {
if (k.options?.validate) {
if (k.options?.required && !target[k.key]) {
return;
} else if (k.options?.required || target[k.key]) {
if (!k.options.validate(target[k.key])) return;
} else {
if (!target[k.key]) return;
}
}
}

View File

@ -61,8 +61,9 @@ CREATE TABLE IF NOT EXISTS team_draft (
-- death
CREATE TABLE IF NOT EXISTS death (
id INT AUTO_INCREMENT PRIMARY KEY,
message VARCHAR(1024) NOT NULL,
dead_user_id INT NOT NULL,
dead_user_id INT NOT NULL UNIQUE,
killer_user_id INT,
FOREIGN KEY (dead_user_id) REFERENCES user(id) ON DELETE CASCADE,
FOREIGN KEY (killer_user_id) REFERENCES user(id) ON DELETE CASCADE

View File

@ -83,9 +83,13 @@ import {
setSettings
} from './schema/settings';
import {
addDeath,
type AddDeathReq,
addDeath,
death,
deleteDeath,
type DeleteDeathReq,
editDeath,
type EditDeathReq,
getDeathByUserId,
type GetDeathByUserIdReq,
getDeaths,
@ -273,6 +277,8 @@ export class Database {
/* death */
addDeath = (values: AddDeathReq) => addDeath(this.db, values);
editDeath = (values: EditDeathReq) => editDeath(this.db, values);
deleteDeath = (values: DeleteDeathReq) => deleteDeath(this.db, values);
getDeathByUserId = (values: GetDeathByUserIdReq) => getDeathByUserId(this.db, values);
getDeaths = (values: GetDeathsReq) => getDeaths(this.db, values);

View File

@ -6,6 +6,7 @@ import { eq } from 'drizzle-orm';
type Database = MySql2Database<{ death: typeof death }>;
export const death = mysqlTable('death', {
id: int('id').primaryKey().autoincrement(),
message: varchar('message', { length: 1024 }).notNull(),
deadUserId: int('dead_user_id')
.notNull()
@ -15,10 +16,21 @@ export const death = mysqlTable('death', {
export type AddDeathReq = {
message: string;
killerUserId?: number;
killerUserId?: number | null;
deadUserId: number;
};
export type EditDeathReq = {
id: number;
message: string;
killerUserId?: number | null;
deadUserId: number;
};
export type DeleteDeathReq = {
id: number;
};
export type GetDeathByUserIdReq = {
userId: number;
};
@ -26,7 +38,24 @@ export type GetDeathByUserIdReq = {
export type GetDeathsReq = {};
export async function addDeath(db: Database, values: AddDeathReq) {
await db.insert(death).values(values);
const ids = await db.insert(death).values(values).$returningId();
return ids[0];
}
export async function editDeath(db: Database, values: EditDeathReq) {
await db
.update(death)
.set({
message: values.message,
killerUserId: values.killerUserId,
deadUserId: values.deadUserId
})
.where(eq(death.id, values.id));
}
export async function deleteDeath(db: Database, values: DeleteDeathReq) {
await db.delete(death).where(eq(death.id, values.id));
}
export async function getDeathByUserId(db: Database, values: GetDeathByUserIdReq) {
@ -41,6 +70,7 @@ export async function getDeaths(db: Database, _values: GetDeathsReq) {
return db
.select({
id: death.id,
message: death.message,
killed: {
id: killed.id,

View File

@ -113,6 +113,7 @@ export async function getReports(db: Database, values: GetReportsReq) {
id: report.id,
reason: report.reason,
body: report.body,
urlHash: report.urlHash,
createdAt: report.createdAt,
reporter: {
id: reporterTeam.id,

View File

@ -40,6 +40,13 @@ const adminTabs = [
href: 'admin/teams',
name: 'Teams',
icon: 'heroicons:users',
subTabs: [
{
href: 'admin/teams/dead',
name: 'Tote Spieler',
icon: 'heroicons:x-mark'
}
],
enabled: session?.permissions.users
},
{
@ -72,6 +79,12 @@ const adminTabs = [
name: 'Einstellungen',
icon: 'heroicons:adjustments-horizontal',
enabled: session?.permissions.settings
},
{
href: 'admin/tools',
name: 'Tools',
icon: 'heroicons:wrench-screwdriver',
enabled: session?.permissions.tools
}
];
---

View File

@ -0,0 +1,16 @@
---
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import SidebarActions from '@app/admin/teamDeaths/SidebarActions.svelte';
import TeamDeaths from '@app/admin/teamDeaths/TeamDeaths.svelte';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { BASE_PATH } from 'astro:env/server';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Users);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Tote Spieler">
<SidebarActions slot="actions" client:load />
<TeamDeaths client:load />
</AdminLayout>

View File

@ -0,0 +1,14 @@
---
import { Session } from '@util/session';
import { Permissions } from '@util/permissions';
import { BASE_PATH } from 'astro:env/server';
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import Tools from '@app/admin/tools/Tools.svelte';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Tools);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Reports">
<Tools client:load />
</AdminLayout>

View File

@ -76,6 +76,7 @@ export const PUT: APIRoute = async ({ request }) => {
const report = await tx.addReport({
reporterTeamId: reporterTeam?.team.id,
reportedTeamId: reportedTeam.team.id,
createdAt: new Date(),
reason: parsed.reason,
body: parsed.body
});

View File

@ -12,3 +12,46 @@ export async function getJavaUuid(username: string) {
// prettier-ignore
return `${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}`;
}
// https://github.com/carlop3333/XUIDGrabber/blob/main/grabber.js
export async function getBedrockUuid(username: string): Promise<string> {
const initialPageResponse = await fetch('https://cxkes.me/xbox/xuid');
const initialPageContent = await initialPageResponse.text();
const token = /name="_token"\svalue="(?<token>\w+)"/.exec(initialPageContent)?.groups?.token;
const cookies = initialPageResponse.headers.get('set-cookie')?.split(' ');
if (token === undefined || cookies === undefined || cookies.length < 11) return null;
const requestBody = new URLSearchParams();
requestBody.set('_token', token);
requestBody.set('gamertag', username);
const resultPageResponse = await fetch('https://cxkes.me/xbox/xuid', {
method: 'post',
body: requestBody,
// prettier-ignore
headers: {
'Host': 'www.cxkes.me',
'Accept-Encoding': 'gzip, deflate,br',
'Content-Length': Buffer.byteLength(requestBody.toString()).toString(),
'Origin': 'https://www.cxkes.me',
'DNT': '1',
'Connection': 'keep-alive',
'Referer': 'https://www.cxkes.me/xbox/xuid',
'Cookie': `${cookies[0]} ${cookies[10].slice(0, cookies[10].length - 1)}`,
'Upgrade-Insecure-Requests': '1',
'Sec-Fectch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,es;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const resultPageContent = await resultPageResponse.text();
let xuid: string | undefined;
if ((xuid = /id="xuidHex">(?<xuid>\w+)</.exec(resultPageContent)?.groups?.xuid) === undefined) throw new Error();
return `00000000-0000-0000-${xuid.substring(0, 4)}-${xuid.substring(4)}`;
}

View File

@ -53,10 +53,10 @@ export class Permissions {
return (this.value & Permissions.Reports.value) != 0;
}
get feedback() {
return (this.value & Permissions.Reports.value) != 0;
return (this.value & Permissions.Feedback.value) != 0;
}
get settings() {
return (this.value & Permissions.Reports.value) != 0;
return (this.value & Permissions.Settings.value) != 0;
}
get tools() {
return (this.value & Permissions.Tools.value) != 0;