diff --git a/middleware/auth.ts b/middleware/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..caeee180f10ba045b8a3571a77f65bd2bc488839 --- /dev/null +++ b/middleware/auth.ts @@ -0,0 +1,6 @@ +export default defineNuxtRouteMiddleware((to) => { + const isAuthenticated = false; // TODO: check if user is authenticated + if (!isAuthenticated && to.path.startsWith('/admin')) { + return navigateTo('/login'); + } +}); \ No newline at end of file diff --git a/server/api/auth/login.ts b/server/api/auth/login.ts new file mode 100644 index 0000000000000000000000000000000000000000..858c241964ac8ca027e0673aae204bc0d3d845e2 --- /dev/null +++ b/server/api/auth/login.ts @@ -0,0 +1,23 @@ +import { compare } from 'bcrypt'; +import { prisma } from '~/server/prisma'; + +export default defineEventHandler(async (event) => { + const userExists = await prisma.user.count(); + if (!userExists) { + throw createError({ statusCode: 403, statusMessage: 'No users exist' }); + } + + const { email, password } = await readBody(event); + + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user || !(await compare(password, user.password))) { + throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' }); + } + + // Set up session or token logic here + + return { user }; +}); \ No newline at end of file diff --git a/server/api/auth/register.ts b/server/api/auth/register.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ecf56fcd33e67d99824a88909057178fb7db3db --- /dev/null +++ b/server/api/auth/register.ts @@ -0,0 +1,29 @@ +import { hash } from 'bcrypt'; +import { prisma } from '~/server/prisma'; + +export default defineEventHandler(async (event) => { + const userExists = await prisma.user.count(); + if (userExists) { + throw createError({ statusCode: 403, statusMessage: 'User already exists' }); + } + + const { email, name, password } = await readBody(event); + + if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password) || !/[^A-Za-z0-9]/.test(password)) { + return new Response('Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', { + status: 422, + }); + } + + const hashedPassword = await hash(password, 10); + + const user = await prisma.user.create({ + data: { + email, + name, + password: hashedPassword, + }, + }); + + return { user }; +}); \ No newline at end of file diff --git a/server/api/auth/userExists.ts b/server/api/auth/userExists.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0ecb413787fda099dc878bdb0f81997a7b3fb2b --- /dev/null +++ b/server/api/auth/userExists.ts @@ -0,0 +1,6 @@ +import {prisma} from "~/server/prisma"; + +export default defineEventHandler(async (event) => { + const userCount = await prisma.user.count(); + return { userExists: userCount > 0 }; +}) \ No newline at end of file diff --git a/server/prisma.ts b/server/prisma.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e0833460e2b22fe334c2d643417e59384f20566 --- /dev/null +++ b/server/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export { prisma }; \ No newline at end of file diff --git a/stores/auth.ts b/stores/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0c899fa00d53b0df9cae5a735e05423d06f5f64 --- /dev/null +++ b/stores/auth.ts @@ -0,0 +1,64 @@ +import { defineStore } from 'pinia'; +import { useRouter } from 'vue-router'; + +export const useAuthStore = defineStore('auth', { + state: () => ({ + user: null, + }), + + actions: { + async login(email: string, password: string) { + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + // Promise.reject() is used to avoid "'throw' of exception caught locally" warning + // https://stackoverflow.com/a/60725482/12680199 + return Promise.reject(Error('Invalid credentials')); + } + + this.user = await response.json(); + const router = useRouter(); + await router.push('/admin'); + } catch (error) { + console.error(error); + } + }, + + async register(email: string, password: string, name: string) { + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password, name }), + }); + + if (!response.ok) { + // Promise.reject() is used to avoid "'throw' of exception caught locally" warning + // https://stackoverflow.com/a/60725482/12680199 + return Promise.reject(Error('Error registering user')); + } + + this.user = await response.json(); + const router = useRouter(); + await router.push('/admin'); + } catch (error) { + console.error(error); + } + }, + + async logout() { + this.user = null; + const router = useRouter(); + await router.push('/'); + } + } +}) \ No newline at end of file