add direct invitation link
All checks were successful
deploy / build-and-deploy (push) Successful in 21s
All checks were successful
deploy / build-and-deploy (push) Successful in 21s
This commit is contained in:
@@ -17,19 +17,33 @@ export const signup = {
|
|||||||
.max(Date.now() - 1000 * 60 * 60 * 24 * 365 * 6),
|
.max(Date.now() - 1000 * 60 * 60 * 24 * 365 * 6),
|
||||||
phone: z.string().trim().nullable(),
|
phone: z.string().trim().nullable(),
|
||||||
username: z.string().trim(),
|
username: z.string().trim(),
|
||||||
edition: z.enum(['java', 'bedrock'])
|
edition: z.enum(['java', 'bedrock']),
|
||||||
|
hash: z.string().nullish()
|
||||||
}),
|
}),
|
||||||
handler: async (input) => {
|
handler: async (input) => {
|
||||||
|
// check if hash given and if so, if it's valid and nobody has already used it
|
||||||
|
if (input.hash) {
|
||||||
|
const directSignup = await db.getDirectSignupByHash({ hash: input.hash });
|
||||||
|
|
||||||
|
if (!directSignup) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'NOT_FOUND'
|
||||||
|
});
|
||||||
|
} else if (directSignup.user) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'CONFLICT'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if signup is allowed
|
// check if signup is allowed
|
||||||
if (!(await getSetting(db, SettingKey.SignupEnabled))) {
|
else if (!(await getSetting(db, SettingKey.SignupEnabled))) {
|
||||||
throw new ActionError({
|
throw new ActionError({
|
||||||
code: 'FORBIDDEN',
|
code: 'FORBIDDEN',
|
||||||
message: 'Die Anmeldung ist derzeit deaktiviert'
|
message: 'Die Anmeldung ist derzeit deaktiviert'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the user were already signed up
|
// check if the user were already signed up
|
||||||
if (await db.getUserByUsername({ username: input.username })) {
|
else if (await db.getUserByUsername({ username: input.username })) {
|
||||||
throw new ActionError({
|
throw new ActionError({
|
||||||
code: 'CONFLICT',
|
code: 'CONFLICT',
|
||||||
message: 'Du hast dich bereits registriert'
|
message: 'Du hast dich bereits registriert'
|
||||||
@@ -57,14 +71,20 @@ export const signup = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.addUser({
|
await db.transaction(async (tx) => {
|
||||||
firstname: input.firstname,
|
const user = await tx.addUser({
|
||||||
lastname: input.lastname,
|
firstname: input.firstname,
|
||||||
birthday: input.birthday,
|
lastname: input.lastname,
|
||||||
telephone: input.phone,
|
birthday: input.birthday,
|
||||||
username: input.username,
|
telephone: input.phone,
|
||||||
edition: input.edition,
|
username: input.username,
|
||||||
uuid: uuid
|
edition: input.edition,
|
||||||
|
uuid: uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input.hash) {
|
||||||
|
await tx.setDirectSignupUser({ hash: input.hash, userId: user.id });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sendWebhook(WebhookAction.Signup, {
|
sendWebhook(WebhookAction.Signup, {
|
||||||
|
|||||||
@@ -144,5 +144,31 @@ export const user = {
|
|||||||
blocked: await db.getBlockedUsers({})
|
blocked: await db.getBlockedUsers({})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
addDirectInvitation: defineAction({
|
||||||
|
handler: async (_, context) => {
|
||||||
|
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
|
||||||
|
|
||||||
|
return await db.addDirectSignup({});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
deleteDirectInvitation: defineAction({
|
||||||
|
input: z.object({
|
||||||
|
hash: z.string()
|
||||||
|
}),
|
||||||
|
handler: async (input, context) => {
|
||||||
|
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
|
||||||
|
|
||||||
|
await db.deleteDirectSignup({ hash: input.hash });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
directInvitations: defineAction({
|
||||||
|
handler: async (_, context) => {
|
||||||
|
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
|
||||||
|
|
||||||
|
return {
|
||||||
|
directInvitations: await db.getDirectSignups({})
|
||||||
|
};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
28
src/app/admin/directInvitations/DirectInvitations.svelte
Normal file
28
src/app/admin/directInvitations/DirectInvitations.svelte
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DataTable from '@components/admin/table/DataTable.svelte';
|
||||||
|
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||||
|
import {
|
||||||
|
deleteDirectInvitation,
|
||||||
|
type DirectInvitation,
|
||||||
|
directInvitations
|
||||||
|
} from '@app/admin/directInvitations/directInvitations.ts';
|
||||||
|
|
||||||
|
// callback
|
||||||
|
function onDirectInvitationDelete(directInvitation: DirectInvitation) {
|
||||||
|
$confirmPopupState = {
|
||||||
|
title: 'Direkte Einladung löschen?',
|
||||||
|
message: 'Soll die direkte Einladung wirklich entblockiert werden?',
|
||||||
|
onConfirm: () => deleteDirectInvitation(directInvitation)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
data={directInvitations}
|
||||||
|
count={true}
|
||||||
|
keys={[
|
||||||
|
{ key: 'url', label: 'Link', width: 50 },
|
||||||
|
{ key: 'user.username', label: 'Registrierter Nutzer', width: 40, sortable: true }
|
||||||
|
]}
|
||||||
|
onDelete={onDirectInvitationDelete}
|
||||||
|
/>
|
||||||
32
src/app/admin/directInvitations/SidebarActions.svelte
Normal file
32
src/app/admin/directInvitations/SidebarActions.svelte
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from '@iconify/svelte';
|
||||||
|
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||||
|
import { addDirectInvitation, fetchDirectInvitations } from '@app/admin/directInvitations/directInvitations.ts';
|
||||||
|
|
||||||
|
// states
|
||||||
|
let createPopupOpen = $state(false);
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
$effect(() => {
|
||||||
|
fetchDirectInvitations();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||||
|
<Icon icon="heroicons:plus-16-solid" />
|
||||||
|
<span>Neue direkte Einladung</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CrudPopup
|
||||||
|
texts={{
|
||||||
|
title: 'Direkte Einladung erstellen',
|
||||||
|
submitButtonTitle: 'Erstellen',
|
||||||
|
confirmPopupTitle: 'Direkte Einladung erstellen',
|
||||||
|
confirmPopupMessage: 'Bist du sicher, dass die direkte Einladung erstellt werden soll?.'
|
||||||
|
}}
|
||||||
|
target={null}
|
||||||
|
onSubmit={addDirectInvitation}
|
||||||
|
bind:open={createPopupOpen}
|
||||||
|
/>
|
||||||
45
src/app/admin/directInvitations/directInvitations.ts
Normal file
45
src/app/admin/directInvitations/directInvitations.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { type ActionReturnType, actions } from 'astro:actions';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { actionErrorPopup } from '@util/action.ts';
|
||||||
|
import { addToWritableArray, deleteFromWritableArray } from '@util/state.ts';
|
||||||
|
|
||||||
|
// types
|
||||||
|
export type DirectInvitations = Exclude<
|
||||||
|
ActionReturnType<typeof actions.user.directInvitations>['data'],
|
||||||
|
undefined
|
||||||
|
>['directInvitations'];
|
||||||
|
export type DirectInvitation = DirectInvitations[0];
|
||||||
|
|
||||||
|
// state
|
||||||
|
export const directInvitations = writable<DirectInvitations>([]);
|
||||||
|
|
||||||
|
// actions
|
||||||
|
export async function fetchDirectInvitations() {
|
||||||
|
const { data, error } = await actions.user.directInvitations();
|
||||||
|
if (error) {
|
||||||
|
actionErrorPopup(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
directInvitations.set(data.directInvitations);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addDirectInvitation() {
|
||||||
|
const { data, error } = await actions.user.addDirectInvitation();
|
||||||
|
if (error) {
|
||||||
|
actionErrorPopup(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToWritableArray(directInvitations, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDirectInvitation(directInvitation: DirectInvitation) {
|
||||||
|
const { error } = await actions.user.deleteDirectInvitation(directInvitation);
|
||||||
|
if (error) {
|
||||||
|
actionErrorPopup(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFromWritableArray(directInvitations, (i) => i.hash === directInvitation.hash);
|
||||||
|
}
|
||||||
@@ -8,9 +8,10 @@
|
|||||||
paypalLink: string;
|
paypalLink: string;
|
||||||
teamspeakLink: string;
|
teamspeakLink: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
|
newRegistrationAllowed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { discordLink, paypalLink, teamspeakLink, startDate }: Props = $props();
|
let { discordLink, paypalLink, teamspeakLink, startDate, newRegistrationAllowed }: Props = $props();
|
||||||
|
|
||||||
let skin: string | null = $state(null);
|
let skin: string | null = $state(null);
|
||||||
|
|
||||||
@@ -87,10 +88,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
{#if newRegistrationAllowed}
|
||||||
<div class="flex justify-center gap-8">
|
<div class="divider"></div>
|
||||||
<button class="btn">Weitere Person anmelden</button>
|
<div class="flex justify-center gap-8">
|
||||||
</div>
|
<button class="btn">Weitere Person anmelden</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
<div class="absolute w-full h-full bg-black/50"></div>
|
<div class="absolute w-full h-full bg-black/50"></div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -6,28 +6,27 @@ import Select from '@components/input/Select.svelte';
|
|||||||
import RulesPopup from '@app/website/signup/RulesPopup.svelte';
|
import RulesPopup from '@app/website/signup/RulesPopup.svelte';
|
||||||
import Popup from '@components/popup/Popup.svelte';
|
import Popup from '@components/popup/Popup.svelte';
|
||||||
import RegisteredPopup from '@app/website/signup/RegisteredPopup.svelte';
|
import RegisteredPopup from '@app/website/signup/RegisteredPopup.svelte';
|
||||||
import { getSettings, SettingKey } from '@util/settings';
|
|
||||||
import { db } from '@db/database.ts';
|
|
||||||
import { DISCORD_LINK, PAYPAL_LINK, START_DATE, TEAMSPEAK_LINK } from 'astro:env/server';
|
import { DISCORD_LINK, PAYPAL_LINK, START_DATE, TEAMSPEAK_LINK } from 'astro:env/server';
|
||||||
|
|
||||||
const signupSetting = await getSettings(db, [
|
interface Props {
|
||||||
SettingKey.SignupEnabled,
|
signupDisabled?: {
|
||||||
SettingKey.SignupDisabledMessage,
|
message: string;
|
||||||
SettingKey.SignupDisabledSubMessage
|
subMessage?: string;
|
||||||
]);
|
};
|
||||||
const signupEnabled = signupSetting[SettingKey.SignupEnabled] ?? false;
|
signupHash?: string;
|
||||||
const signupDisabledMessage = signupSetting[SettingKey.SignupDisabledMessage] ?? 'Anmeldung deaktiviert';
|
}
|
||||||
const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessage] ?? '';
|
|
||||||
|
const { signupDisabled, signupHash } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<WebsiteLayout title="Anmeldung" footer={false}>
|
<WebsiteLayout title="Anmeldung" footer={false}>
|
||||||
<div
|
<div
|
||||||
class="flex justify-center w-full min-h-screen bg-base-200"
|
class="flex justify-center w-full min-h-screen bg-base-200"
|
||||||
class:list={[!signupEnabled ? 'max-h-screen overflow-hidden' : undefined]}
|
class:list={[signupDisabled ? 'max-h-screen overflow-hidden' : undefined]}
|
||||||
>
|
>
|
||||||
<div class="relative grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 my-12 bg-base-100 shadow-lg h-min">
|
<div class="relative grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 my-12 bg-base-100 shadow-lg h-min">
|
||||||
<h1 class="text-center text-3xl lg:text-5xl">Anmeldung</h1>
|
<h1 class="text-center text-3xl lg:text-5xl">Anmeldung</h1>
|
||||||
<form id="signup">
|
<form id="signup" data-signup-hash={signupHash}>
|
||||||
<div class="divider">Persönliche Angaben</div>
|
<div class="divider">Persönliche Angaben</div>
|
||||||
<div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-x-4">
|
<div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-x-4">
|
||||||
<Input
|
<Input
|
||||||
@@ -141,22 +140,23 @@ const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessa
|
|||||||
paypalLink={PAYPAL_LINK}
|
paypalLink={PAYPAL_LINK}
|
||||||
teamspeakLink={TEAMSPEAK_LINK}
|
teamspeakLink={TEAMSPEAK_LINK}
|
||||||
startDate={START_DATE}
|
startDate={START_DATE}
|
||||||
|
newRegistrationAllowed={!signupHash}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!signupEnabled && (
|
signupDisabled && (
|
||||||
<div class="absolute top-0 left-0 w-full h-full bg-black/50 backdrop-blur-sm z-10 rounded-xl flex justify-center items-center flex-col pt-20 lg:pt-0 text-2xl sm:text-4xl">
|
<div class="absolute top-0 left-0 w-full h-full bg-black/50 backdrop-blur-sm z-10 rounded-xl flex justify-center items-center flex-col pt-20 lg:pt-0 text-2xl sm:text-4xl">
|
||||||
<h1>{signupDisabledMessage}</h1>
|
<h1>{signupDisabled.message}</h1>
|
||||||
<h3>{signupDisabledSubMessage}</h3>
|
<h3>{signupDisabled.subMessage}</h3>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { actions } from 'astro:actions';
|
import { actions } from 'astro:actions';
|
||||||
import { popupState } from '@components/popup/Popup';
|
import { popupState } from '@components/popup/Popup.ts';
|
||||||
import { rulesPopupState, rulesPopupRead } from '@app/website/signup/RulesPopup';
|
import { rulesPopupState, rulesPopupRead } from '@app/website/signup/RulesPopup.ts';
|
||||||
import { registeredPopupState } from '@app/website/signup/RegisteredPopup';
|
import { registeredPopupState } from '@app/website/signup/RegisteredPopup.ts';
|
||||||
|
|
||||||
function setupClientValidation() {
|
function setupClientValidation() {
|
||||||
const rulesCheckbox = document.getElementById('rules') as HTMLInputElement;
|
const rulesCheckbox = document.getElementById('rules') as HTMLInputElement;
|
||||||
@@ -192,7 +192,8 @@ const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessa
|
|||||||
birthday: form.birthday.value,
|
birthday: form.birthday.value,
|
||||||
phone: form.phone.value,
|
phone: form.phone.value,
|
||||||
username: form.username.value,
|
username: form.username.value,
|
||||||
edition: form.edition.value
|
edition: form.edition.value,
|
||||||
|
hash: form.dataset.signupHash
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
target: T | null;
|
target: T | null;
|
||||||
keys: Key<any>[][];
|
keys?: Key<any>[][];
|
||||||
|
|
||||||
onSubmit: (target: T) => void;
|
onSubmit: (target: T) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let { texts, target, keys, onSubmit, onClose, open = $bindable() }: Props<any> = $props();
|
let { texts, target, keys = [], onSubmit, onClose, open = $bindable() }: Props<any> = $props();
|
||||||
|
|
||||||
onInit();
|
onInit();
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ CREATE TABLE IF NOT EXISTS blocked_user (
|
|||||||
comment TINYTEXT
|
comment TINYTEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- direct signup
|
||||||
|
CREATE TABLE IF NOT EXISTS direct_signup (
|
||||||
|
hash CHAR(255) NOT NULL UNIQUE,
|
||||||
|
user_id INT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
-- report
|
-- report
|
||||||
CREATE TABLE IF NOT EXISTS report (
|
CREATE TABLE IF NOT EXISTS report (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|||||||
@@ -119,12 +119,26 @@ import {
|
|||||||
type GetReportAttachmentsReq,
|
type GetReportAttachmentsReq,
|
||||||
reportAttachment
|
reportAttachment
|
||||||
} from '@db/schema/reportAttachment.ts';
|
} from '@db/schema/reportAttachment.ts';
|
||||||
|
import {
|
||||||
|
addDirectSignup,
|
||||||
|
type AddDirectSignupReq,
|
||||||
|
deleteDirectSignup,
|
||||||
|
type DeleteDirectSignupReq,
|
||||||
|
directSignup,
|
||||||
|
getDirectSignupByHash,
|
||||||
|
type GetDirectSignupByHashReq,
|
||||||
|
getDirectSignups,
|
||||||
|
type GetDirectSignupsReq,
|
||||||
|
setDirectSignupUser,
|
||||||
|
type SetDirectSignupUserReq
|
||||||
|
} from '@db/schema/directSignup.ts';
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
protected readonly db: MySql2Database<{
|
protected readonly db: MySql2Database<{
|
||||||
admin: typeof admin;
|
admin: typeof admin;
|
||||||
user: typeof user;
|
user: typeof user;
|
||||||
blockedUser: typeof blockedUser;
|
blockedUser: typeof blockedUser;
|
||||||
|
directSignup: typeof directSignup;
|
||||||
report: typeof report;
|
report: typeof report;
|
||||||
reportAttachment: typeof reportAttachment;
|
reportAttachment: typeof reportAttachment;
|
||||||
reportStatus: typeof reportStatus;
|
reportStatus: typeof reportStatus;
|
||||||
@@ -149,6 +163,7 @@ export class Database {
|
|||||||
admin,
|
admin,
|
||||||
user,
|
user,
|
||||||
blockedUser,
|
blockedUser,
|
||||||
|
directSignup,
|
||||||
report,
|
report,
|
||||||
reportAttachment,
|
reportAttachment,
|
||||||
reportStatus,
|
reportStatus,
|
||||||
@@ -191,6 +206,13 @@ export class Database {
|
|||||||
getBlockedUserByUuid = (values: GetBlockedUserByUuidReq) => getBlockedUserByUuid(this.db, values);
|
getBlockedUserByUuid = (values: GetBlockedUserByUuidReq) => getBlockedUserByUuid(this.db, values);
|
||||||
getBlockedUsers = (values: GetBlockedUsersReq) => getBlockedUsers(this.db, values);
|
getBlockedUsers = (values: GetBlockedUsersReq) => getBlockedUsers(this.db, values);
|
||||||
|
|
||||||
|
/* direct signup */
|
||||||
|
addDirectSignup = (values: AddDirectSignupReq) => addDirectSignup(this.db, values);
|
||||||
|
setDirectSignupUser = (values: SetDirectSignupUserReq) => setDirectSignupUser(this.db, values);
|
||||||
|
deleteDirectSignup = (values: DeleteDirectSignupReq) => deleteDirectSignup(this.db, values);
|
||||||
|
getDirectSignups = (values: GetDirectSignupsReq) => getDirectSignups(this.db, values);
|
||||||
|
getDirectSignupByHash = (values: GetDirectSignupByHashReq) => getDirectSignupByHash(this.db, values);
|
||||||
|
|
||||||
/* report */
|
/* report */
|
||||||
addReport = (values: AddReportReq) => addReport(this.db, values);
|
addReport = (values: AddReportReq) => addReport(this.db, values);
|
||||||
editReport = (values: EditReportReq) => editReport(this.db, values);
|
editReport = (values: EditReportReq) => editReport(this.db, values);
|
||||||
|
|||||||
79
src/db/schema/directSignup.ts
Normal file
79
src/db/schema/directSignup.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
||||||
|
import { int, mysqlTable, varchar } from 'drizzle-orm/mysql-core';
|
||||||
|
import { user } from '@db/schema/user.ts';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { generateRandomString } from '@util/random.ts';
|
||||||
|
import { BASE_PATH } from 'astro:env/server';
|
||||||
|
|
||||||
|
type Database = MySql2Database<{ directSignup: typeof directSignup }>;
|
||||||
|
|
||||||
|
export const directSignup = mysqlTable('direct_signup', {
|
||||||
|
hash: varchar('hash', { length: 255 }).unique().notNull(),
|
||||||
|
userId: int('user_id').references(() => user.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AddDirectSignupReq = {};
|
||||||
|
|
||||||
|
export type SetDirectSignupUserReq = {
|
||||||
|
hash: string;
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteDirectSignupReq = {
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetDirectSignupsReq = {};
|
||||||
|
|
||||||
|
export type GetDirectSignupByHashReq = {
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function addDirectSignup(db: Database, _values: AddDirectSignupReq) {
|
||||||
|
const hash = generateRandomString(16);
|
||||||
|
|
||||||
|
await db.insert(directSignup).values({
|
||||||
|
hash: hash
|
||||||
|
});
|
||||||
|
|
||||||
|
return { hash: hash, url: `${BASE_PATH}/signup/${hash}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setDirectSignupUser(db: Database, values: SetDirectSignupUserReq) {
|
||||||
|
return db
|
||||||
|
.update(directSignup)
|
||||||
|
.set({
|
||||||
|
userId: values.userId
|
||||||
|
})
|
||||||
|
.where(eq(directSignup.hash, values.hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDirectSignup(db: Database, values: DeleteDirectSignupReq) {
|
||||||
|
return db.delete(directSignup).where(eq(directSignup.hash, values.hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDirectSignups(db: Database, _values: GetDirectSignupsReq) {
|
||||||
|
const directSignups = await db
|
||||||
|
.select({
|
||||||
|
hash: directSignup.hash,
|
||||||
|
user: user
|
||||||
|
})
|
||||||
|
.from(directSignup)
|
||||||
|
.leftJoin(user, eq(directSignup.userId, user.id));
|
||||||
|
|
||||||
|
return directSignups.map((d) => ({ ...d, url: `${BASE_PATH}/signup/${d.hash}` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDirectSignupByHash(db: Database, values: GetDirectSignupByHashReq) {
|
||||||
|
const directSignups = await db
|
||||||
|
.select({
|
||||||
|
hash: directSignup.hash,
|
||||||
|
user: user
|
||||||
|
})
|
||||||
|
.from(directSignup)
|
||||||
|
.leftJoin(user, eq(directSignup.userId, user.id))
|
||||||
|
.where(eq(directSignup.hash, values.hash))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return directSignups[0] ?? null;
|
||||||
|
}
|
||||||
@@ -28,6 +28,11 @@ const adminTabs = [
|
|||||||
name: 'Registrierte Nutzer',
|
name: 'Registrierte Nutzer',
|
||||||
icon: 'heroicons:user',
|
icon: 'heroicons:user',
|
||||||
subTabs: [
|
subTabs: [
|
||||||
|
{
|
||||||
|
href: 'admin/users/direct_invitations',
|
||||||
|
name: 'Direkte Einladungen',
|
||||||
|
icon: 'heroicons:envelope'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: 'admin/users/blocked',
|
href: 'admin/users/blocked',
|
||||||
name: 'Blockierte Nutzer',
|
name: 'Blockierte Nutzer',
|
||||||
@@ -79,7 +84,7 @@ const adminTabs = [
|
|||||||
<BaseLayout title={title}>
|
<BaseLayout title={title}>
|
||||||
<ClientRouter />
|
<ClientRouter />
|
||||||
<div class="flex flex-row max-h-[100vh]">
|
<div class="flex flex-row max-h-[100vh]">
|
||||||
<ul class="menu bg-base-200 w-68 h-[100vh] flex">
|
<ul class="menu bg-base-200 w-70 h-[100vh] flex">
|
||||||
{
|
{
|
||||||
preTabs.map((tab) => (
|
preTabs.map((tab) => (
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
16
src/pages/admin/users/direct_invitations.astro
Normal file
16
src/pages/admin/users/direct_invitations.astro
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
import { Session } from '@util/session.ts';
|
||||||
|
import { Permissions } from '@util/permissions.ts';
|
||||||
|
import { BASE_PATH } from 'astro:env/server';
|
||||||
|
import AdminLayout from '@layouts/admin/AdminLayout.astro';
|
||||||
|
import SidebarActions from '@app/admin/directInvitations/SidebarActions.svelte';
|
||||||
|
import DirectInvitations from '@app/admin/directInvitations/DirectInvitations.svelte';
|
||||||
|
|
||||||
|
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Reports);
|
||||||
|
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
||||||
|
---
|
||||||
|
|
||||||
|
<AdminLayout title="Direkte Einladungen">
|
||||||
|
<SidebarActions slot="actions" client:load />
|
||||||
|
<DirectInvitations client:load />
|
||||||
|
</AdminLayout>
|
||||||
18
src/pages/signup/[...hash].astro
Normal file
18
src/pages/signup/[...hash].astro
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
import { db } from '@db/database.ts';
|
||||||
|
import Signup from '@app/website/signup/Signup.astro';
|
||||||
|
|
||||||
|
const { hash } = Astro.params;
|
||||||
|
|
||||||
|
const directSignup = await db.getDirectSignupByHash({ hash: hash! });
|
||||||
|
if (!directSignup) return new Response(null, { status: 404 });
|
||||||
|
---
|
||||||
|
|
||||||
|
<Signup
|
||||||
|
signupHash={hash}
|
||||||
|
signupDisabled={directSignup.user
|
||||||
|
? {
|
||||||
|
message: 'Es hat sich bereits jemand mit diesem Einladungslink registriert'
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
|
/>
|
||||||
24
src/pages/signup/index.astro
Normal file
24
src/pages/signup/index.astro
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import { getSettings, SettingKey } from '@util/settings.ts';
|
||||||
|
import { db } from '@db/database.ts';
|
||||||
|
import Signup from '@app/website/signup/Signup.astro';
|
||||||
|
|
||||||
|
const signupSetting = await getSettings(db, [
|
||||||
|
SettingKey.SignupEnabled,
|
||||||
|
SettingKey.SignupDisabledMessage,
|
||||||
|
SettingKey.SignupDisabledSubMessage
|
||||||
|
]);
|
||||||
|
const signupEnabled = signupSetting[SettingKey.SignupEnabled] ?? false;
|
||||||
|
const signupDisabledMessage = signupSetting[SettingKey.SignupDisabledMessage] ?? 'Anmeldung deaktiviert';
|
||||||
|
const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessage] ?? '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Signup
|
||||||
|
signupDisabled={signupEnabled
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
message: signupDisabledMessage,
|
||||||
|
subMessage: signupDisabledSubMessage
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
/>
|
||||||
Reference in New Issue
Block a user