Add login and register
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
export type DateInputDate = Date | string | number;
|
||||||
|
export type DateInputRange<T> = {
|
||||||
|
start?: T | null;
|
||||||
|
end?: T | null;
|
||||||
|
};
|
||||||
|
export type DateInputModelValue =
|
||||||
|
| DateInputDate
|
||||||
|
| DateInputDate[]
|
||||||
|
| DateInputRange<DateInputDate>
|
||||||
|
| undefined
|
||||||
|
| null
|
||||||
|
| string;
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const { t } = useI18n();
|
||||||
|
return {
|
||||||
|
required(name: string) {
|
||||||
|
return (value: string) =>
|
||||||
|
(value && value.length > 0) || t('validation.required', { name });
|
||||||
|
},
|
||||||
|
max(name: string, max = 64) {
|
||||||
|
return (value: string) =>
|
||||||
|
(value && value.length <= max) ||
|
||||||
|
t('validation.max_length', { name, max });
|
||||||
|
},
|
||||||
|
min(name: string, min = 8) {
|
||||||
|
return (value: string) =>
|
||||||
|
(value && value.length >= min) ||
|
||||||
|
t('validation.min_length', { name, min });
|
||||||
|
},
|
||||||
|
dateRangeIsPast() {
|
||||||
|
return (model: DateInputModelValue) =>
|
||||||
|
(model && isRangeModel(model) && model.start && model.end
|
||||||
|
? new Date(model.start).getTime() < Date.now() &&
|
||||||
|
new Date(model.end).getTime() < Date.now()
|
||||||
|
: true) || t('validation.date_range_past');
|
||||||
|
},
|
||||||
|
dateIsPast() {
|
||||||
|
return (model: DateInputModelValue) =>
|
||||||
|
(model &&
|
||||||
|
isDateModel(model) &&
|
||||||
|
new Date(model).getTime() < Date.now()) ||
|
||||||
|
t('validation.date_range_past');
|
||||||
|
},
|
||||||
|
numeric(name: string) {
|
||||||
|
return (value: string) =>
|
||||||
|
(value &&
|
||||||
|
!isNaN(parseFloat(value)) &&
|
||||||
|
value?.split(' ').length === 1) ||
|
||||||
|
t('validation.numeric', { name });
|
||||||
|
},
|
||||||
|
numericOptional(name: string) {
|
||||||
|
return (value: string | undefined) =>
|
||||||
|
!value ||
|
||||||
|
(!isNaN(parseFloat(value)) && value?.split(' ').length === 1) ||
|
||||||
|
t('validation.numeric', { name });
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRangeModel(
|
||||||
|
model: DateInputModelValue
|
||||||
|
): model is DateInputRange<DateInputDate> {
|
||||||
|
return (
|
||||||
|
!!model &&
|
||||||
|
typeof model === 'object' &&
|
||||||
|
(model as DateInputRange<any>)?.end !== undefined &&
|
||||||
|
(model as DateInputRange<any>)?.start !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDateModel(model: DateInputModelValue): model is DateInputDate {
|
||||||
|
return !!model && !isNaN(new Date(model as any).getTime());
|
||||||
|
}
|
||||||
+25
-1
@@ -6,7 +6,19 @@
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
"logout": "Abmelden"
|
"login_instead": "Stattdessen anmelden",
|
||||||
|
"register": "Registrieren",
|
||||||
|
"register_instead": "Stattdessen registrieren",
|
||||||
|
"logout": "Abmelden",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"password": "Passwort",
|
||||||
|
"name": "Name",
|
||||||
|
"accept": "Akzeptieren",
|
||||||
|
"showPassword": "Passwort einblenden",
|
||||||
|
"hidePassword": "Passwort ausblenden",
|
||||||
|
"reset": "Zurücksetzen",
|
||||||
|
"conflict": "Diese Email wird bereits verwendet, bitte verwende eine andere!",
|
||||||
|
"invalid": "Email oder Passwort sind falsch, bitte versuche es erneut!"
|
||||||
},
|
},
|
||||||
"layouts": {
|
"layouts": {
|
||||||
"title": "{title} {'|'} PicHunt",
|
"title": "{title} {'|'} PicHunt",
|
||||||
@@ -20,6 +32,18 @@
|
|||||||
"default": "Unbekannter Fehler",
|
"default": "Unbekannter Fehler",
|
||||||
"team_not_found": "Team nicht gefunden oder Passwort inkorrekt!"
|
"team_not_found": "Team nicht gefunden oder Passwort inkorrekt!"
|
||||||
},
|
},
|
||||||
|
"validation": {
|
||||||
|
"max_length": "{name} darf maximal {max} Zeichen lang sein",
|
||||||
|
"min_length": "{name} muss mindestens {min} Zeichen lang sein",
|
||||||
|
"numeric": "{name} muss numerisch sein",
|
||||||
|
"min_array": "Du brauchst mindestens {min} {name}",
|
||||||
|
"required": "{name} ist erforderlich",
|
||||||
|
"invalid": "Ungültiges Format",
|
||||||
|
"taken": "Bereits vergeben",
|
||||||
|
"date_range_past": "Zeitraum darf nicht in der Zukunft liegen",
|
||||||
|
"date_past": "Zeit darf nicht in der Zukunft liegen",
|
||||||
|
"confirm": "Sind Sie sich sicher?"
|
||||||
|
},
|
||||||
"vuestic": {
|
"vuestic": {
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"noOptions": "Items not found",
|
"noOptions": "Items not found",
|
||||||
|
|||||||
+25
-1
@@ -6,7 +6,19 @@
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"logout": "Logout"
|
"login_instead": "Login instead",
|
||||||
|
"register": "Register",
|
||||||
|
"register_instead": "Register instead",
|
||||||
|
"logout": "Logout",
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password",
|
||||||
|
"name": "Name",
|
||||||
|
"accept": "Accept",
|
||||||
|
"showPassword": "Show password",
|
||||||
|
"hidePassword": "Hide password",
|
||||||
|
"reset": "Reset",
|
||||||
|
"conflict": "This e-mail is already in use, please use a different one!",
|
||||||
|
"invalid": "E-mail or password is invalid, please try again!"
|
||||||
},
|
},
|
||||||
"layouts": {
|
"layouts": {
|
||||||
"title": "{title} {'|'} PicHunt",
|
"title": "{title} {'|'} PicHunt",
|
||||||
@@ -20,6 +32,18 @@
|
|||||||
"default": "Unkown error",
|
"default": "Unkown error",
|
||||||
"team_not_found": "Team not found or password incorrect!"
|
"team_not_found": "Team not found or password incorrect!"
|
||||||
},
|
},
|
||||||
|
"validation": {
|
||||||
|
"max_length": "{name} has to be max {max} chars long",
|
||||||
|
"min_length": "{name} has to at least {min} chars long",
|
||||||
|
"numeric": "{name} need to be numeric",
|
||||||
|
"min_array": "You need at least {min} {name}",
|
||||||
|
"required": "{name} is required",
|
||||||
|
"invalid": "Invalid format",
|
||||||
|
"taken": "Already taken",
|
||||||
|
"date_range_past": "Date range cannot be in the future",
|
||||||
|
"date_past": "Date cannot be in the future",
|
||||||
|
"confirm": "Are you sure?"
|
||||||
|
},
|
||||||
"vuestic": {
|
"vuestic": {
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"noOptions": "Items not found",
|
"noOptions": "Items not found",
|
||||||
|
|||||||
+112
-19
@@ -1,37 +1,130 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from '#app';
|
import { useForm } from 'vuestic-ui';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['guest']
|
middleware: ['guest']
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fetch } = useUserSession();
|
const { fetch } = useUserSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const localePath = useLocalePath();
|
||||||
// TODO: remove this lol
|
const { isValid, resetValidation, validate } = useForm('formRef');
|
||||||
const { data } = await useFetch('/api/auth/test-list');
|
const validation = useValidation();
|
||||||
|
|
||||||
async function login(id: number) {
|
const form = reactive({
|
||||||
await $fetch(`/api/auth/test?id=${id}`);
|
email: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
|
||||||
await fetch();
|
const error = ref('');
|
||||||
|
|
||||||
await router.push(
|
async function submit() {
|
||||||
(route.query.to
|
if (!validate()) {
|
||||||
? typeof route.query.to === 'string'
|
return;
|
||||||
? route.query.to
|
}
|
||||||
: route.query.to[0]
|
error.value = '';
|
||||||
: '/') || '/'
|
try {
|
||||||
);
|
await $fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
await fetch();
|
||||||
|
await router.push(localePath('/'));
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && 'statusCode' in e && e.statusCode === 404) {
|
||||||
|
error.value = 'auth.invalid';
|
||||||
|
} else {
|
||||||
|
error.value = parseError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form.password = '';
|
||||||
|
resetValidation();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>login</h1>
|
<h1 class="mb-4 text-center text-3xl">{{ $t('auth.login') }}</h1>
|
||||||
<div class="flex flex-col gap-2" v-if="data">
|
<div class="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5">
|
||||||
<VaButton v-for="user in data" :key="user.id" @click="login(user.id)"
|
<VaForm
|
||||||
>Login as "{{ user.name }}" {{ user.email }}</VaButton
|
ref="formRef"
|
||||||
|
tag="form"
|
||||||
|
class="flex flex-col items-baseline gap-6 sm:col-start-2 lg:col-start-3"
|
||||||
|
@submit.prevent="submit"
|
||||||
>
|
>
|
||||||
|
<VaInput
|
||||||
|
v-model="form.email"
|
||||||
|
class="w-full"
|
||||||
|
type="email"
|
||||||
|
:label="$t('auth.email')"
|
||||||
|
autocomplete="email"
|
||||||
|
:rules="[validation.required($t('auth.email'))]"
|
||||||
|
:placeholder="$t('auth.email')"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prependInner>
|
||||||
|
<VaIcon name="mail_outline" color="secondary" />
|
||||||
|
</template>
|
||||||
|
</VaInput>
|
||||||
|
<VaValue v-slot="isPasswordVisible" :default-value="false">
|
||||||
|
<VaInput
|
||||||
|
v-model="form.password"
|
||||||
|
class="w-full"
|
||||||
|
:type="isPasswordVisible.value ? 'text' : 'password'"
|
||||||
|
:label="$t('auth.password')"
|
||||||
|
:placeholder="$t('auth.password')"
|
||||||
|
:rules="[
|
||||||
|
validation.required($t('auth.password')),
|
||||||
|
validation.min($t('auth.password'), 8)
|
||||||
|
]"
|
||||||
|
autocomplete="current-password"
|
||||||
|
:minlength="8"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prependInner>
|
||||||
|
<VaIcon name="key" color="secondary" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner>
|
||||||
|
<VaButton
|
||||||
|
v-if="form.password"
|
||||||
|
preset="plain"
|
||||||
|
:title="
|
||||||
|
$t(
|
||||||
|
isPasswordVisible.value
|
||||||
|
? 'auth.hidePassword'
|
||||||
|
: 'auth.showPassword'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="isPasswordVisible.value = !isPasswordVisible.value"
|
||||||
|
>
|
||||||
|
<VaIcon
|
||||||
|
:name="
|
||||||
|
isPasswordVisible.value ? 'visibility_off' : 'visibility'
|
||||||
|
"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</VaButton>
|
||||||
|
</template>
|
||||||
|
</VaInput>
|
||||||
|
</VaValue>
|
||||||
|
<VaAlert
|
||||||
|
v-if="error.length > 0"
|
||||||
|
color="danger"
|
||||||
|
class="w-full text-center"
|
||||||
|
>{{ $t(error) }}</VaAlert
|
||||||
|
>
|
||||||
|
<VaButtonGroup>
|
||||||
|
<VaButton color="success" type="submit" :disabled="!isValid">{{
|
||||||
|
$t('auth.login')
|
||||||
|
}}</VaButton>
|
||||||
|
<VaButton
|
||||||
|
:to="$localePath('/register')"
|
||||||
|
border-color="primary"
|
||||||
|
preset="secondary"
|
||||||
|
>{{ $t('auth.register_instead') }}</VaButton
|
||||||
|
>
|
||||||
|
</VaButtonGroup>
|
||||||
|
</VaForm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getRandomName } from '#shared/utils/name';
|
||||||
|
import { useForm } from 'vuestic-ui';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['guest']
|
||||||
|
});
|
||||||
|
const { fetch } = useUserSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
const { isValid, resetValidation, validate } = useForm('formRef');
|
||||||
|
const validation = useValidation();
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
name: getRandomName(),
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await $fetch('/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
await fetch();
|
||||||
|
await router.push(localePath('/'));
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && 'statusCode' in e && e.statusCode === 409) {
|
||||||
|
error.value = 'auth.conflict';
|
||||||
|
} else {
|
||||||
|
error.value = parseError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form.password = '';
|
||||||
|
resetValidation();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="mb-4 text-center text-3xl">{{ $t('auth.login') }}</h1>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5">
|
||||||
|
<VaForm
|
||||||
|
ref="formRef"
|
||||||
|
tag="form"
|
||||||
|
class="flex flex-col items-baseline gap-6 sm:col-start-2 lg:col-start-3"
|
||||||
|
@submit.prevent="submit"
|
||||||
|
>
|
||||||
|
<VaInput
|
||||||
|
v-model="form.name"
|
||||||
|
class="w-full"
|
||||||
|
type="text"
|
||||||
|
autocomplete="name"
|
||||||
|
clearable
|
||||||
|
clear-value=""
|
||||||
|
:label="$t('auth.name')"
|
||||||
|
:placeholder="$t('auth.name')"
|
||||||
|
max-length="64"
|
||||||
|
min-length="5"
|
||||||
|
:rules="[
|
||||||
|
validation.required($t('auth.name')),
|
||||||
|
validation.max($t('auth.name'), 64),
|
||||||
|
validation.min($t('auth.name'), 5)
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prependInner>
|
||||||
|
<VaIcon name="person" color="secondary" />
|
||||||
|
</template>
|
||||||
|
<template #appendInner>
|
||||||
|
<VaButton
|
||||||
|
icon="refresh"
|
||||||
|
preset="plain"
|
||||||
|
@click="form.name = getRandomName()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VaInput>
|
||||||
|
<VaInput
|
||||||
|
v-model="form.email"
|
||||||
|
class="w-full"
|
||||||
|
type="email"
|
||||||
|
:label="$t('auth.email')"
|
||||||
|
autocomplete="email"
|
||||||
|
:rules="[validation.required($t('auth.email'))]"
|
||||||
|
:placeholder="$t('auth.email')"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prependInner>
|
||||||
|
<VaIcon name="mail_outline" color="secondary" />
|
||||||
|
</template>
|
||||||
|
</VaInput>
|
||||||
|
<VaValue v-slot="isPasswordVisible" :default-value="false">
|
||||||
|
<VaInput
|
||||||
|
v-model="form.password"
|
||||||
|
class="w-full"
|
||||||
|
:type="isPasswordVisible.value ? 'text' : 'password'"
|
||||||
|
:label="$t('auth.password')"
|
||||||
|
:placeholder="$t('auth.password')"
|
||||||
|
:rules="[
|
||||||
|
validation.required($t('auth.password')),
|
||||||
|
validation.min($t('auth.password'), 8)
|
||||||
|
]"
|
||||||
|
autocomplete="current-password"
|
||||||
|
:minlength="8"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prependInner>
|
||||||
|
<VaIcon name="key" color="secondary" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner>
|
||||||
|
<VaButton
|
||||||
|
v-if="form.password"
|
||||||
|
preset="plain"
|
||||||
|
:title="
|
||||||
|
$t(
|
||||||
|
isPasswordVisible.value
|
||||||
|
? 'auth.hidePassword'
|
||||||
|
: 'auth.showPassword'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="isPasswordVisible.value = !isPasswordVisible.value"
|
||||||
|
>
|
||||||
|
<VaIcon
|
||||||
|
:name="
|
||||||
|
isPasswordVisible.value ? 'visibility_off' : 'visibility'
|
||||||
|
"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</VaButton>
|
||||||
|
</template>
|
||||||
|
</VaInput>
|
||||||
|
</VaValue>
|
||||||
|
<VaAlert
|
||||||
|
v-if="error.length > 0"
|
||||||
|
color="danger"
|
||||||
|
class="w-full text-center"
|
||||||
|
>{{ $t(error) }}</VaAlert
|
||||||
|
>
|
||||||
|
|
||||||
|
<VaButtonGroup>
|
||||||
|
<VaButton color="success" type="submit" :disabled="!isValid">{{
|
||||||
|
$t('auth.register')
|
||||||
|
}}</VaButton>
|
||||||
|
<VaButton
|
||||||
|
:to="$localePath('/login')"
|
||||||
|
border-color="primary"
|
||||||
|
preset="secondary"
|
||||||
|
>{{ $t('auth.login_instead') }}</VaButton
|
||||||
|
>
|
||||||
|
</VaButtonGroup>
|
||||||
|
</VaForm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"nuxt-auth-utils": "^0.5.22",
|
"nuxt-auth-utils": "^0.5.22",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
|
"unique-names-generator": "^4.7.1",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"zod": "^4.0.0",
|
"zod": "^4.0.0",
|
||||||
@@ -1890,6 +1891,8 @@
|
|||||||
|
|
||||||
"unimport": ["unimport@5.2.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.1", "magic-string": "^0.30.17", "mlly": "^1.7.4", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.2.0", "scule": "^1.3.0", "strip-literal": "^3.0.0", "tinyglobby": "^0.2.14", "unplugin": "^2.3.5", "unplugin-utils": "^0.2.4" } }, "sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw=="],
|
"unimport": ["unimport@5.2.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.1", "magic-string": "^0.30.17", "mlly": "^1.7.4", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.2.0", "scule": "^1.3.0", "strip-literal": "^3.0.0", "tinyglobby": "^0.2.14", "unplugin": "^2.3.5", "unplugin-utils": "^0.2.4" } }, "sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw=="],
|
||||||
|
|
||||||
|
"unique-names-generator": ["unique-names-generator@4.7.1", "", {}, "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow=="],
|
||||||
|
|
||||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|
||||||
"unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="],
|
"unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="],
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"nuxt-auth-utils": "^0.5.22",
|
"nuxt-auth-utils": "^0.5.22",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
|
"unique-names-generator": "^4.7.1",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"zod": "^4.0.0"
|
"zod": "^4.0.0"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { hash } from 'argon2';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
@@ -6,6 +7,7 @@ const prisma = new PrismaClient();
|
|||||||
async function main() {
|
async function main() {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
// region Users
|
// region Users
|
||||||
|
const password = await hash('12345678');
|
||||||
const admin = await tx.user.upsert({
|
const admin = await tx.user.upsert({
|
||||||
where: { email: 'admin@mail.com' },
|
where: { email: 'admin@mail.com' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -13,6 +15,7 @@ async function main() {
|
|||||||
email: 'admin@mail.com',
|
email: 'admin@mail.com',
|
||||||
name: 'Admin user',
|
name: 'Admin user',
|
||||||
role: 'ADMIN',
|
role: 'ADMIN',
|
||||||
|
password,
|
||||||
emailConfirmedAt: new Date()
|
emailConfirmedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -23,6 +26,7 @@ async function main() {
|
|||||||
email: 'team@mail.com',
|
email: 'team@mail.com',
|
||||||
name: 'Hunt member',
|
name: 'Hunt member',
|
||||||
role: 'ADMIN',
|
role: 'ADMIN',
|
||||||
|
password,
|
||||||
emailConfirmedAt: new Date()
|
emailConfirmedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -34,6 +38,7 @@ async function main() {
|
|||||||
email: 'user@mail.com',
|
email: 'user@mail.com',
|
||||||
name: 'User',
|
name: 'User',
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
|
password,
|
||||||
emailConfirmedAt: new Date()
|
emailConfirmedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -44,6 +49,7 @@ async function main() {
|
|||||||
email: 'member@mail.com',
|
email: 'member@mail.com',
|
||||||
name: 'Team Member',
|
name: 'Team Member',
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
|
password,
|
||||||
emailConfirmedAt: new Date()
|
emailConfirmedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { verify } from 'argon2';
|
||||||
|
import { useValidatedBody, z } from 'h3-zod';
|
||||||
|
import prisma from '~~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { email, password } = await useValidatedBody(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
email: z.email().min(5).trim(),
|
||||||
|
password: z.string().min(8)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
email
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
password: true,
|
||||||
|
name: true,
|
||||||
|
role: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user || user.password.length <= 0) {
|
||||||
|
console.warn(`Failed login attempted: Unknown email: ${email}`);
|
||||||
|
throw createError({ status: 404, statusText: 'Not found!' });
|
||||||
|
}
|
||||||
|
if (!(await verify(user.password, password))) {
|
||||||
|
console.warn(`Failed login attempted: Wrong password: ${email}`);
|
||||||
|
throw createError({ status: 404, statusText: 'Not found!' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Successful login attempted: ${email}`);
|
||||||
|
await setUserSession(event as any, {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role
|
||||||
|
},
|
||||||
|
loggedInAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, user: { id: user.id } };
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { hash } from 'argon2';
|
||||||
|
import { useValidatedBody, z } from 'h3-zod';
|
||||||
|
import prisma from '~~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
await clearUserSession(event as any);
|
||||||
|
|
||||||
|
const { email, password, name } = await useValidatedBody(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
email: z.email().min(5).trim(),
|
||||||
|
password: z.string().min(8),
|
||||||
|
name: z.string().min(5).max(64).trim()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const exists = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email
|
||||||
|
},
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exists !== null) {
|
||||||
|
throw createError({
|
||||||
|
status: 409,
|
||||||
|
statusText: 'User with that email already exists!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await hash(password);
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
password: passwordHash,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
},
|
||||||
|
select: { id: true, name: true, role: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn(`Successful register: ${email}`);
|
||||||
|
|
||||||
|
await setUserSession(event, {
|
||||||
|
user: user
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, user: { id: user.id } };
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
adjectives,
|
||||||
|
animals,
|
||||||
|
colors,
|
||||||
|
type Config,
|
||||||
|
uniqueNamesGenerator
|
||||||
|
} from 'unique-names-generator';
|
||||||
|
|
||||||
|
const nameConfig: Config = {
|
||||||
|
dictionaries: [adjectives, colors, animals],
|
||||||
|
separator: ' ',
|
||||||
|
style: 'capital'
|
||||||
|
};
|
||||||
|
export function getRandomName() {
|
||||||
|
return uniqueNamesGenerator(nameConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomUsername() {
|
||||||
|
return uniqueNamesGenerator({ ...nameConfig, separator: '' });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user