-
Login as "{{ user.name }}" {{ user.email }}{{ $t('auth.login') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t(error) }}
+
+ {{
+ $t('auth.login')
+ }}
+ {{ $t('auth.register_instead') }}
+
+
diff --git a/app/pages/register.vue b/app/pages/register.vue
new file mode 100644
index 0000000..804c8bc
--- /dev/null
+++ b/app/pages/register.vue
@@ -0,0 +1,163 @@
+
+
+
+ {{ $t('auth.login') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t(error) }}
+
+
+ {{
+ $t('auth.register')
+ }}
+ {{ $t('auth.login_instead') }}
+
+
+
+
+
+
diff --git a/bun.lock b/bun.lock
index c7b7257..35b8e12 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,6 +17,7 @@
"nuxt-auth-utils": "^0.5.22",
"pinia": "^3.0.3",
"slugify": "^1.6.6",
+ "unique-names-generator": "^4.7.1",
"vue": "^3.5.18",
"vue-router": "^4.5.1",
"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=="],
+ "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=="],
"unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="],
diff --git a/package.json b/package.json
index f350dde..ba5adaf 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"nuxt-auth-utils": "^0.5.22",
"pinia": "^3.0.3",
"slugify": "^1.6.6",
+ "unique-names-generator": "^4.7.1",
"vue": "^3.5.18",
"vue-router": "^4.5.1",
"zod": "^4.0.0"
diff --git a/prisma/seed/hunt.ts b/prisma/seed/hunt.ts
index e1e1e8e..1480b6c 100644
--- a/prisma/seed/hunt.ts
+++ b/prisma/seed/hunt.ts
@@ -1,4 +1,5 @@
import { PrismaClient } from '@prisma/client';
+import { hash } from 'argon2';
import { randomUUID } from 'node:crypto';
const prisma = new PrismaClient();
@@ -6,6 +7,7 @@ const prisma = new PrismaClient();
async function main() {
await prisma.$transaction(async (tx) => {
// region Users
+ const password = await hash('12345678');
const admin = await tx.user.upsert({
where: { email: 'admin@mail.com' },
update: {},
@@ -13,6 +15,7 @@ async function main() {
email: 'admin@mail.com',
name: 'Admin user',
role: 'ADMIN',
+ password,
emailConfirmedAt: new Date()
}
});
@@ -23,6 +26,7 @@ async function main() {
email: 'team@mail.com',
name: 'Hunt member',
role: 'ADMIN',
+ password,
emailConfirmedAt: new Date()
}
});
@@ -34,6 +38,7 @@ async function main() {
email: 'user@mail.com',
name: 'User',
role: 'USER',
+ password,
emailConfirmedAt: new Date()
}
});
@@ -44,6 +49,7 @@ async function main() {
email: 'member@mail.com',
name: 'Team Member',
role: 'USER',
+ password,
emailConfirmedAt: new Date()
}
});
diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts
new file mode 100644
index 0000000..3de7a24
--- /dev/null
+++ b/server/api/auth/login.post.ts
@@ -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 } };
+});
diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts
new file mode 100644
index 0000000..f626def
--- /dev/null
+++ b/server/api/auth/register.post.ts
@@ -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 } };
+});
diff --git a/shared/utils/name.ts b/shared/utils/name.ts
new file mode 100644
index 0000000..9b2dc55
--- /dev/null
+++ b/shared/utils/name.ts
@@ -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: '' });
+}