diff --git a/package.json b/package.json
index f6290d7..aa63c02 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,10 @@
},
"type": "module",
"dependencies": {
+ "@lucia-auth/adapter-prisma": "^0.4.0",
+ "@lucia-auth/sveltekit": "^0.6.2",
"@picocss/pico": "^1.5.7",
- "@prisma/client": "^4.9.0"
+ "@prisma/client": "^4.9.0",
+ "lucia-auth": "^0.6.0"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5fb03b0..aaf97af 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,6 +1,8 @@
lockfileVersion: 5.4
specifiers:
+ '@lucia-auth/adapter-prisma': ^0.4.0
+ '@lucia-auth/sveltekit': ^0.6.2
'@picocss/pico': ^1.5.7
'@playwright/test': ^1.30.0
'@prisma/client': ^4.9.0
@@ -11,6 +13,7 @@ specifiers:
eslint: ^8.33.0
eslint-config-prettier: ^8.6.0
eslint-plugin-svelte3: ^4.0.0
+ lucia-auth: ^0.6.0
prettier: ^2.8.3
prettier-plugin-svelte: ^2.9.0
prisma: ^4.9.0
@@ -23,8 +26,11 @@ specifiers:
vitest: ^0.25.8
dependencies:
+ '@lucia-auth/adapter-prisma': 0.4.0_lucia-auth@0.6.0
+ '@lucia-auth/sveltekit': 0.6.2_iyasqsbplmktfcni36hpxhd3zi
'@picocss/pico': 1.5.7
'@prisma/client': 4.9.0_prisma@4.9.0
+ lucia-auth: 0.6.0
devDependencies:
'@playwright/test': 1.30.0
@@ -299,6 +305,29 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
+ /@lucia-auth/adapter-prisma/0.4.0_lucia-auth@0.6.0:
+ resolution: {integrity: sha512-HMaGbVfB5KTZBs6KPqI5z6RFKVl4AKfm9u2KrrQglthpd/rWJoJRDI2oYkdcEX4Xu1FNdNJJrEtKKBtVAXYqrw==}
+ peerDependencies:
+ lucia-auth: 0.6.x
+ dependencies:
+ lucia-auth: 0.6.0
+ dev: false
+
+ /@lucia-auth/sveltekit/0.6.2_iyasqsbplmktfcni36hpxhd3zi:
+ resolution: {integrity: sha512-+lOhgctcdVkPRYtJegTaEZYLFhPJOHFjdAm9cBFFjpkV6cNGuHSTxobfOn7yRy3pGVhzGFM91uOfYrJxmECOBA==}
+ peerDependencies:
+ lucia-auth: 0.5.x - 0.6.x
+ svelte: 3.x
+ dependencies:
+ '@noble/hashes': 1.2.0
+ lucia-auth: 0.6.0
+ svelte: 3.55.1
+ dev: false
+
+ /@noble/hashes/1.2.0:
+ resolution: {integrity: sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==}
+ dev: false
+
/@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1321,6 +1350,13 @@ packages:
yallist: 4.0.0
dev: true
+ /lucia-auth/0.6.0:
+ resolution: {integrity: sha512-8j5nPl3RbbqGoZWULER4q+2PP7i8F3Eq3OeN7EnL+bxi4YAn5I+5FcNq/ikUCj8neOxGZyzJiRzFJq+t3r28+g==}
+ dependencies:
+ '@noble/hashes': 1.2.0
+ nanoid: 4.0.1
+ dev: false
+
/magic-string/0.27.0:
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
engines: {node: '>=12'}
@@ -1389,6 +1425,12 @@ packages:
hasBin: true
dev: true
+ /nanoid/4.0.1:
+ resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==}
+ engines: {node: ^14 || ^16 || >=18}
+ hasBin: true
+ dev: false
+
/natural-compare-lite/1.4.0:
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
dev: true
@@ -1807,7 +1849,6 @@ packages:
/svelte/3.55.1:
resolution: {integrity: sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==}
engines: {node: '>= 8'}
- dev: true
/text-table/0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6d8beb7..4e2128a 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -14,4 +14,39 @@ model Article {
id Int @id @default(autoincrement())
title String
content String
+ User User @relation(fields: [userId], references: [id])
+ userId String
+}
+
+model User {
+ id String @id @unique
+ name String
+ username String @unique
+ articles Article[]
+ session Session[]
+ Key Key[]
+
+ @@map("user")
+}
+
+model Session {
+ id String @id @unique
+ user_id String
+ active_expires BigInt
+ idle_expires BigInt
+ user User @relation(references: [id], fields: [user_id], onDelete: Cascade)
+
+ @@index([user_id])
+ @@map("session")
+}
+
+model Key {
+ id String @id @unique
+ hashed_password String?
+ user_id String
+ primary Boolean
+ user User @relation(references: [id], fields: [user_id], onDelete: Cascade)
+
+ @@index([user_id])
+ @@map("key")
}
diff --git a/src/app.d.ts b/src/app.d.ts
index 19ea674..cc85c67 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -3,11 +3,24 @@ import type { PrismaClient } from "@prisma/client"
declare global {
namespace App {
// interface Error {}
- // interface Locals {}
+ interface Locals {
+ validate: import("@lucia-auth/sveltekit").Validate
+ validateUser: import("@lucia-auth/sveltekit").ValidateUser
+ setSession: import("@lucia-auth/sveltekit").SetSession
+ }
// interface PageData {}
// interface Platform {}
}
var __prisma: PrismaClient
+
+ ///
+ declare namespace Lucia {
+ type Auth = import("$lib/server/lucia").Auth
+ type UserAttributes = {
+ username: string
+ name: string
+ }
+ }
}
export {}
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
new file mode 100644
index 0000000..5727024
--- /dev/null
+++ b/src/hooks.server.ts
@@ -0,0 +1,10 @@
+import { handleHooks } from "@lucia-auth/sveltekit"
+import { auth } from "$lib/server/lucia"
+import type { Handle } from "@sveltejs/kit"
+import { sequence } from "@sveltejs/kit/hooks"
+
+export const customHandle: Handle = async ({ resolve, event }) => {
+ return resolve(event)
+}
+
+export const handle: Handle = sequence(handleHooks(auth), customHandle)
diff --git a/src/lib/server/lucia.ts b/src/lib/server/lucia.ts
new file mode 100644
index 0000000..3afd315
--- /dev/null
+++ b/src/lib/server/lucia.ts
@@ -0,0 +1,18 @@
+import lucia from "lucia-auth"
+import prismaAdapter from "@lucia-auth/adapter-prisma"
+import { dev } from "$app/environment"
+import { prisma } from "$lib/server/prisma"
+
+export const auth = lucia({
+ adapter: prismaAdapter(prisma),
+ env: dev ? "DEV" : "PROD",
+ transformUserData: (userData) => {
+ return {
+ userId: userData.id,
+ username: userData.username,
+ name: userData.name,
+ }
+ },
+})
+
+export type Auth = typeof auth
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index b2103e1..fb5c940 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -1,3 +1,6 @@
import type { LayoutServerLoad } from "./$types"
-export const load: LayoutServerLoad = async () => {}
+export const load: LayoutServerLoad = async ({ locals }) => {
+ const { user, session } = await locals.validateUser()
+ return { user }
+}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index b82c45d..d8b5e68 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,5 +1,7 @@
@@ -12,9 +14,17 @@
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
index 6993761..29f3e88 100644
--- a/src/routes/+page.server.ts
+++ b/src/routes/+page.server.ts
@@ -1,6 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"
import { prisma } from "$lib/server/prisma"
-import { fail } from "@sveltejs/kit"
+import { error, fail, redirect } from "@sveltejs/kit"
export const load: PageServerLoad = async () => {
return {
@@ -9,7 +9,12 @@ export const load: PageServerLoad = async () => {
}
export const actions: Actions = {
- createArticle: async ({ request }) => {
+ createArticle: async ({ request, locals }) => {
+ const { user, session } = await locals.validateUser()
+ if (!(user && session)) {
+ throw redirect(302, "/")
+ }
+
const { title, content } = Object.fromEntries(
await request.formData(),
) as Record
@@ -19,6 +24,7 @@ export const actions: Actions = {
data: {
title,
content,
+ userId: user.userId,
},
})
} catch (err) {
@@ -30,13 +36,27 @@ export const actions: Actions = {
status: 201,
}
},
- deleteArticle: async ({ url }) => {
+ deleteArticle: async ({ url, locals }) => {
+ const { user, session } = await locals.validateUser()
+ if (!(user && session)) {
+ throw redirect(302, "/")
+ }
const id = url.searchParams.get("id")
if (!id) {
return fail(400, { message: "Invalid request" })
}
try {
+ const article = await prisma.article.findUniqueOrThrow({
+ where: {
+ id: Number(id),
+ },
+ })
+
+ if (article.userId !== user.userId) {
+ throw error(403, "Not authorized")
+ }
+
await prisma.article.delete({
where: {
id: Number(id),
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index e6ec5b1..e465d23 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -15,21 +15,25 @@
{article.content}
-
- Edit Article
+ {#if article.userId === data.user?.userId}
+
+ Edit Article
+ {/if}
{/each}
-
+ {#if data.user}
+
+ {/if}
diff --git a/src/routes/[articleId]/+page.server.ts b/src/routes/[articleId]/+page.server.ts
index aa0c711..c664bf2 100644
--- a/src/routes/[articleId]/+page.server.ts
+++ b/src/routes/[articleId]/+page.server.ts
@@ -2,8 +2,13 @@ import type { Actions, PageServerLoad } from "./$types"
import { prisma } from "$lib/server/prisma"
import { error, fail } from "@sveltejs/kit"
-export const load: PageServerLoad = async ({ params }) => {
- const getArticle = async () => {
+export const load: PageServerLoad = async ({ params, locals }) => {
+ const { user, session } = await locals.validateUser()
+ if (!(user && session)) {
+ throw error(401, "Unauthorized")
+ }
+
+ const getArticle = async (userId: string) => {
const article = await prisma.article.findUnique({
where: {
id: Number(params.articleId),
@@ -12,21 +17,39 @@ export const load: PageServerLoad = async ({ params }) => {
if (!article) {
throw error(404, "Article not found")
}
+ if (article.userId !== user.userId) {
+ throw error(403, "Unauthorized")
+ }
+
return article
}
return {
- article: getArticle(),
+ article: getArticle(user.userId),
}
}
export const actions: Actions = {
- updateArticle: async ({ request, params }) => {
+ updateArticle: async ({ request, params, locals }) => {
+ const { user, session } = await locals.validateUser()
+ if (!(user && session)) {
+ throw error(401, "Unauthorized")
+ }
+
const { title, content } = Object.fromEntries(
await request.formData(),
) as Record
try {
+ const article = await prisma.article.findUniqueOrThrow({
+ where: {
+ id: Number(params.articleId),
+ },
+ })
+
+ if (article.userId !== user.userId) {
+ throw error(403, "Forbidden to edit this article.")
+ }
await prisma.article.update({
where: {
id: Number(params.articleId),
diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts
index e69de29..7b51207 100644
--- a/src/routes/login/+page.server.ts
+++ b/src/routes/login/+page.server.ts
@@ -0,0 +1,28 @@
+import { auth } from "$lib/server/lucia"
+import { fail, redirect } from "@sveltejs/kit"
+import type { Actions, PageServerLoad } from "./$types"
+
+export const load: PageServerLoad = async ({ locals }) => {
+ const session = await locals.validate()
+ if (session) {
+ throw redirect(302, "/")
+ }
+}
+
+export const actions: Actions = {
+ default: async ({ request, locals }) => {
+ const { username, password } = Object.fromEntries(
+ await request.formData(),
+ ) as Record
+
+ try {
+ const key = await auth.validateKeyPassword("username", username, password)
+ const session = await auth.createSession(key.userId)
+ locals.setSession(session)
+ } catch (err) {
+ console.error(err)
+ return fail(400, { message: "Could not login user." })
+ }
+ throw redirect(302, "/")
+ },
+}
diff --git a/src/routes/logout/+server.ts b/src/routes/logout/+server.ts
index 7480a95..8cda0e7 100644
--- a/src/routes/logout/+server.ts
+++ b/src/routes/logout/+server.ts
@@ -1,5 +1,15 @@
+import { auth } from "$lib/server/lucia"
+import { redirect } from "@sveltejs/kit"
import type { RequestHandler } from "./$types"
-export const POST: RequestHandler = async () => {
- return new Response()
+export const POST: RequestHandler = async ({ locals }) => {
+ const session = await locals.validate()
+ if (!session) {
+ throw redirect(302, "/")
+ }
+
+ await auth.invalidateSession(session.sessionId)
+ locals.setSession(null)
+
+ throw redirect(302, "/")
}
diff --git a/src/routes/register/+page.server.ts b/src/routes/register/+page.server.ts
index e69de29..c24652e 100644
--- a/src/routes/register/+page.server.ts
+++ b/src/routes/register/+page.server.ts
@@ -0,0 +1,36 @@
+import { auth } from "$lib/server/lucia"
+import { fail, redirect } from "@sveltejs/kit"
+import type { Actions, PageServerLoad } from "./$types"
+
+export const load: PageServerLoad = async ({ locals }) => {
+ const session = await locals.validate()
+ if (session) {
+ throw redirect(302, "/")
+ }
+}
+
+export const actions: Actions = {
+ default: async ({ request }) => {
+ const { name, username, password } = Object.fromEntries(
+ await request.formData(),
+ ) as Record
+
+ try {
+ await auth.createUser({
+ key: {
+ providerId: "username",
+ providerUserId: username,
+ password,
+ },
+ attributes: {
+ name,
+ username,
+ },
+ })
+ } catch (err) {
+ console.error(err)
+ return fail(400, { message: "Could not register user" })
+ }
+ throw redirect(302, "/login")
+ },
+}