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