diff --git a/.env b/.env new file mode 100644 index 0000000..af5fec9 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_R2_BASE_URL=https://pub-e115c4e749734702abd09206cba74257.r2.dev/ \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..2febf4b --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +VITE_R2_BASE_URL=/api/ \ No newline at end of file diff --git a/functions/api/avatars/[filename].js b/functions/api/avatars/[filename].js new file mode 100644 index 0000000..9b6ed8a --- /dev/null +++ b/functions/api/avatars/[filename].js @@ -0,0 +1,30 @@ +import { createErrorResponse } from '../../utils'; + +export async function onRequestGet(context) { + try { + const { env, params } = context; + const { filename } = params; + + if (!filename) { + return createErrorResponse("Filename is required", 400); + } + + const key = "avatars/" + filename; + const object = await env.MY_BUCKET.get(key); + + if (object === null) { + return new Response("Object Not Found", { status: 404 }); + } + + const headers = new Headers(); + object.writeHttpMetadata(headers); + headers.set("etag", object.httpEtag); + + return new Response(object.body, { + headers, + }); + } catch (error) { + console.error("Avatar retrieval error:", error); + return createErrorResponse("Server Error", 500); + } +} \ No newline at end of file diff --git a/functions/api/avatars/index.js b/functions/api/avatars/index.js new file mode 100644 index 0000000..9856dab --- /dev/null +++ b/functions/api/avatars/index.js @@ -0,0 +1,50 @@ +import { verifyJWT } from '../../middleware/auth'; +import { createErrorResponse, createSuccessResponse } from '../../utils'; +import { fileTypeFromBuffer } from 'file-type'; + +export async function onRequestPut(context) { + try { + const { request, env } = context; + + // Verify the JWT token + const authResult = await verifyJWT(context); + if (authResult) { + return authResult; // Return the error response from the middleware + } + + const formData = await request.formData(); + const avatar = formData.get('avatar'); + + if (!avatar) { + return createErrorResponse("Missing avatar", 400); + } + + if (avatar.size > 2 * 1024 * 1024) { + return createErrorResponse("Avatar must be less than 2MB", 400); + } + + const buffer = await avatar.arrayBuffer(); + const fileTypeResult = await fileTypeFromBuffer(buffer); + + if (!fileTypeResult) { + return createErrorResponse("Unsupported file type", 400); + } + + if (fileTypeResult.mime !== 'image/jpeg' && fileTypeResult.mime !== 'image/png') { + return createErrorResponse("Avatar must be a JPG or PNG image", 400); + } + + // Upload the avatar to R2 + const fileExtension = fileTypeResult.ext; + const objectName = `avatars/${context.user.userId}.${fileExtension}`; + await env.MY_BUCKET.put(objectName, buffer); + + // Store the filename in D1 + await env.DB.prepare("UPDATE users SET avatar = ? WHERE id = ?").bind(objectName, context.user.userId).run(); + + return createSuccessResponse({ message: "Avatar uploaded successfully" }); + } catch (error) { + console.error("Avatar upload error:", error); + return createErrorResponse("Avatar upload failed", 500); + } +} diff --git a/functions/api/hello.js b/functions/api/hello.js new file mode 100644 index 0000000..656065d --- /dev/null +++ b/functions/api/hello.js @@ -0,0 +1,3 @@ +export function onRequest(context) { + return new Response("Hello, world!") +} \ No newline at end of file diff --git a/functions/api/login.js b/functions/api/login.js new file mode 100644 index 0000000..fad8420 --- /dev/null +++ b/functions/api/login.js @@ -0,0 +1,58 @@ +import { SignJWT } from 'jose'; +import { createSuccessResponse, createErrorResponse } from "../utils"; + +export async function onRequestPost(context) { + try { + const { request, env } = context; + + const { username, password } = await request.json(); + + if (!username || !password) { + return createErrorResponse("Missing username or password", 400); + } + + if (username.length < 3) { + return createErrorResponse("Username must be at least 3 characters", 400); + } + + if (password.length < 8) { + return createErrorResponse("Password must be at least 8 characters", 400); + } + + if (!/^[a-zA-Z0-9]+$/.test(username)) { + return createErrorResponse("Username must be alphanumeric", 400); + } + + // Get the stored password from D1 + const { results } = await env.DB.prepare("SELECT password FROM users WHERE username = ?").bind(username).all(); + + if (!results || results.length === 0) { + return new Response(JSON.stringify({"error": "Invalid username or password"}), { status: 403, headers: { 'Content-Type': 'application/json' } }); + } + + const storedPassword = results[0].password; + + // Compare the password to the stored password + if (password !== storedPassword) { + return new Response(JSON.stringify({"error": "Invalid username or password"}), { status: 403, headers: { 'Content-Type': 'application/json' } }); + } + + // Get the user ID + const { results: userResults } = await env.DB.prepare("SELECT * FROM users WHERE username = ?").bind(username).all(); + const jwtPayload = (({ id, username }) => ({ id, username }))(userResults[0]); + + // Generate a JWT token + const jwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setIssuer('urn:example:issuer') + .setAudience('urn:example:audience') + .setExpirationTime('2h') + .sign(new TextEncoder().encode(env.JWT_SECRET)); + + return createSuccessResponse({ jwt }); + } catch (error) { + console.error("Login error:", error); + return createErrorResponse("Login failed", 500); + } +} diff --git a/functions/api/me.js b/functions/api/me.js new file mode 100644 index 0000000..2da0e9a --- /dev/null +++ b/functions/api/me.js @@ -0,0 +1,30 @@ +import { verifyJWT } from "../middleware/auth"; +import { createErrorResponse, createSuccessResponse } from "../utils"; + +export async function onRequest(context) { + try { + // Verify JWT token + const authResult = await verifyJWT(context); + if (authResult) { + return authResult; // Return error response if authentication fails + } + + // Get user information from context + const { user } = context; + + // Fetch user profile from D1 database + const { results } = await context.env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(user.userId).all(); + if (!results || results.length === 0) { + // use 401 instead to redirect to login page + return createErrorResponse("User not found", 401); + } + const userProfile = results[0]; + const { password, ...profile } = userProfile; // Exclude password from the profile + + // Return the profile as a JSON response + return createSuccessResponse(profile, 200); + } catch (error) { + console.error("Error:", error); + return createErrorResponse("Internal Server Error", 500); + } +} diff --git a/functions/api/messages.js b/functions/api/messages.js new file mode 100644 index 0000000..b8c5dba --- /dev/null +++ b/functions/api/messages.js @@ -0,0 +1,81 @@ +import { verifyJWT } from '../middleware/auth'; +import { createErrorResponse, createSuccessResponse } from '../utils'; + +export async function onRequestGet(context) { + try { + const { env } = context; + + // Get the messages from D1 + const { results } = await env.DB.prepare("SELECT messages.id, userId, username, message, timestamp, users.avatar FROM messages LEFT JOIN users ON users.id = messages.userId").all(); + + return createSuccessResponse({ messages: results }); + } catch (error) { + console.error("Get messages error:", error); + return createErrorResponse("Get messages failed", 500); + } +} + +export async function onRequestPost(context) { + try { + const { request, env } = context; + + // Verify the JWT token + const authResult = await verifyJWT(context); + if (authResult) { + return authResult; // Return the error response from the middleware + } + + const { message } = await request.json(); + + if (!message) { + return new Response(JSON.stringify({"error": "Missing message"}), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Generate a unique ID for the message + const messageId = crypto.randomUUID(); + + // Store the message in D1 + await env.DB.prepare("INSERT INTO messages (id, userId, message) VALUES (?, ?, ?)") + .bind(messageId, context.user.userId, message) + .run(); + + return new Response(JSON.stringify({ id: messageId, username: context.user.username, message }), { + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error("Message posting error:", error); + return createErrorResponse("Message posting failed", 500); + } +} + +export async function onRequestDelete(context) { + try { + const { request, env } = context; + + // Verify the JWT token + const authResult = await verifyJWT(context); + if (authResult) { + return authResult; // Return the error response from the middleware + } + + const { messageId } = await request.json(); + + if (!messageId) { + return new Response(JSON.stringify({"error": "Missing messageId"}), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Delete the message from D1 + await env.DB.prepare("DELETE FROM messages WHERE id = ?").bind(messageId).run(); + + return createSuccessResponse({ message: "Message deleted successfully" }); + } catch (error) { + console.error("Message deletion error:", error); + return createErrorResponse("Message deletion failed", 500); + } +} diff --git a/functions/api/register.js b/functions/api/register.js new file mode 100644 index 0000000..dd14ac2 --- /dev/null +++ b/functions/api/register.js @@ -0,0 +1,52 @@ +import { createErrorResponse } from '../utils'; + +export async function onRequestPost(context) { + try { + const { request, env } = context; + + const { username, password } = await request.json(); + + if (!username || !password) { + return createErrorResponse("Missing username or password", 400); + } + + if (username.length < 3) { + return createErrorResponse("Username must be at least 3 characters", 400); + } + + if (password.length < 8) { + return createErrorResponse("Password must be at least 8 characters", 400); + } + + if (!/^[a-zA-Z0-9]+$/.test(username)) { + return createErrorResponse("Username must be alphanumeric", 400); + } + + // Check if the username already exists + const { results: existingUsers } = await env.DB.prepare("SELECT id FROM users WHERE username = ?").bind(username).all(); + if (existingUsers.length > 0) { + return createErrorResponse("Username already exists", 400); + } + + // Store the username and password in D1 + await env.DB.prepare("INSERT INTO users (username, password, avatar) VALUES (?, ?, ?)").bind(username, password, "avatars/default.png").run(); + + // Get the user ID + const { results } = await env.DB.prepare("SELECT id FROM users WHERE username = ?").bind(username).all(); + const userId = results[0].id; + + // Registration successful, return success response + return new Response( + JSON.stringify({ + success: true, + message: "Registration successful. Please login.", + }), + { + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Registration error:", error); + return createErrorResponse("Server Error", 500); + } +} diff --git a/functions/api/users.js b/functions/api/users.js deleted file mode 100644 index 9af0a37..0000000 --- a/functions/api/users.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @typedef {import('@cloudflare/workers-types').D1Database} D1Database - */ - -export async function onRequestGet(context) { - const DB = context.env.DB; - - // return all users - const stmt = await DB.prepare("SELECT * FROM users"); - const users = (await stmt.run()).results; - - return new Response(JSON.stringify(users), { - headers: { "Content-Type": "application/json" }, - }); -} - -export async function onRequestPost(context) { - /** - * @type {D1Database} - */ - const DB = context.env.DB; - const { name } = await context.request.json(); - - // create a new user - const stmt = DB.prepare("INSERT INTO users (name) VALUES (?)").bind(name); - - try { - const result = await stmt.run(); - - if (!result.success) { - throw new Error("Failed to create user"); - } - - const userId = result.meta.last_row_id; - const stmt2 = await DB.prepare("SELECT * FROM users WHERE id = ?").bind(userId); - const userResult = await stmt2.run(); - const user = userResult.results[0]; - - return new Response(JSON.stringify(user), { - headers: { "Content-Type": "application/json" }, - status: 201, - }); - } catch (error) { - console.error("Error creating user:", error); - return new Response("Error creating user", { status: 500 }); - } -} \ No newline at end of file diff --git a/functions/middleware/auth.js b/functions/middleware/auth.js new file mode 100644 index 0000000..df0175b --- /dev/null +++ b/functions/middleware/auth.js @@ -0,0 +1,26 @@ +import * as jose from 'jose'; +import { createErrorResponse } from "../utils"; + +export async function verifyJWT(context) { + const { request, env } = context; + + // Check for a valid JWT token + const authHeader = request.headers.get("Authorization"); + if (!authHeader) { + return createErrorResponse("Missing Authorization header", 401); + } + + const token = authHeader.split(" ")[1]; + + try { + // Verify the token + const { payload, protectedHeader } = await jose.jwtVerify(token, new TextEncoder().encode(env.JWT_SECRET), { + issuer: 'urn:example:issuer', + audience: 'urn:example:audience', + }); + context.user = { userId: payload.id, username: payload.username }; + return; // Continue to the next middleware or function + } catch (error) { + return createErrorResponse("Invalid or expired token", 401); + } +} diff --git a/functions/utils.js b/functions/utils.js new file mode 100644 index 0000000..4f5b348 --- /dev/null +++ b/functions/utils.js @@ -0,0 +1,13 @@ +export function createErrorResponse(message, status) { + return new Response(JSON.stringify({"error": message}), { + status: status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +export function createSuccessResponse(data, status = 200) { + return new Response(JSON.stringify(data), { + status: status, + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/package-lock.json b/package-lock.json index 5c4d35e..7663810 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "vue", "version": "0.0.0", "dependencies": { + "file-type": "^20.4.1", + "jose": "^6.0.10", + "pinia": "^3.0.2", "vue": "^3.5.13", "vue-router": "^4.5.0" }, @@ -65,9 +68,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20250327.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250327.0.tgz", - "integrity": "sha512-rkoGnSY/GgBLCuhjZMIC3mt0jjqqvL17uOK92OI4eivmE+pMFOAchowDxIWOzDyYe5vwNCakbCeIM/FrSmwGJA==", + "version": "4.20250412.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250412.0.tgz", + "integrity": "sha512-ukQE+TRc5HNkM6VvGfTNC9x54TLQKjdcm624F8Qh1ZRe0iJrW2/j1eYgvJABJPexDousYCR7VzCGteShLcBJYQ==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -806,6 +809,30 @@ "win32" ] }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -883,6 +910,30 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.2.tgz", + "integrity": "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.2", + "birpc": "^0.2.19", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.2.tgz", + "integrity": "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", @@ -960,6 +1011,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -998,12 +1058,44 @@ "fsevents": "~2.3.2" } }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1090,6 +1182,30 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1153,6 +1269,32 @@ "dev": true, "license": "ISC" }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1199,6 +1341,27 @@ "node": ">=0.12.0" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/jose": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz", + "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1245,6 +1408,18 @@ "node": ">=8.6" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1286,6 +1461,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/peek-readable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1305,6 +1499,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.2.tgz", + "integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/@vue/devtools-api": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.2.tgz", + "integrity": "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.2" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -1378,6 +1602,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.36.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz", @@ -1450,6 +1680,44 @@ "node": ">=0.10.0" } }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", + "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1463,6 +1731,35 @@ "node": ">=8.0" } }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -1474,9 +1771,9 @@ } }, "node_modules/vite": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", + "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b22eb75..4f9c7dc 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,16 @@ "scripts": { "dev": "vite --host", "build": "vite build", + "watch": "vite build --watch --mode development", "preview": "vite preview", - "pages:dev": "wrangler pages dev --proxy 5173 -- npm run dev", + "pages:dev": "wrangler pages dev --proxy 5173", "pages:deploy": "wrangler pages deploy dist", "deploy": "npm run build && npm run pages:deploy" }, "dependencies": { + "file-type": "^20.4.1", + "jose": "^6.0.10", + "pinia": "^3.0.2", "vue": "^3.5.13", "vue-router": "^4.5.0" }, diff --git a/schema.sql b/schema.sql index ecc0039..d45f14c 100644 --- a/schema.sql +++ b/schema.sql @@ -1,6 +1,17 @@ +DROP TABLE IF EXISTS messages; DROP TABLE IF EXISTS users; CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(50) NOT NULL UNIQUE -); \ No newline at end of file + username VARCHAR(50) NOT NULL, + password VARCHAR(255) NOT NULL, + avatar VARCHAR(50) +); + +CREATE TABLE messages ( + id VARCHAR(36) PRIMARY KEY NOT NULL, + userId INTEGER NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (userId) REFERENCES users(id) +); diff --git a/src/App.vue b/src/App.vue index 2d25488..c1e33e2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,4 +16,4 @@ import Footer from './components/Footer.vue'; - \ No newline at end of file + diff --git a/src/components/Board/BoardForm.vue b/src/components/Board/BoardForm.vue index b7d072e..3d00bb6 100644 --- a/src/components/Board/BoardForm.vue +++ b/src/components/Board/BoardForm.vue @@ -1,19 +1,23 @@ @@ -22,26 +26,22 @@ function submit() {
- \ No newline at end of file + diff --git a/src/components/Board/BoardMessage.vue b/src/components/Board/BoardMessage.vue index 52f074b..ef73205 100644 --- a/src/components/Board/BoardMessage.vue +++ b/src/components/Board/BoardMessage.vue @@ -1,16 +1,44 @@
+ {{ users }}
-