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