feat: midterm shit done
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_R2_BASE_URL=https://pub-e115c4e749734702abd09206cba74257.r2.dev/
|
||||
@@ -0,0 +1 @@
|
||||
VITE_R2_BASE_URL=/api/
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function onRequest(context) {
|
||||
return new Response("Hello, world!")
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
});
|
||||
}
|
||||
Generated
+303
-6
@@ -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": {
|
||||
|
||||
+5
-1
@@ -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"
|
||||
},
|
||||
|
||||
+13
-2
@@ -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
|
||||
);
|
||||
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)
|
||||
);
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ import Footer from './components/Footer.vue';
|
||||
</RouterView>
|
||||
</div>
|
||||
<Footer />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const emit = defineEmits(['new-message']);
|
||||
const props = defineProps(['locked']);
|
||||
|
||||
const name = ref('');
|
||||
const message = ref('');
|
||||
const text = ref('');
|
||||
const maxLength = 1024;
|
||||
|
||||
const remainingCharacters = computed(() => {
|
||||
return maxLength - text.value.length;
|
||||
});
|
||||
|
||||
function submit() {
|
||||
if (!name || !message) {
|
||||
if (!text.value || props.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('new-message', { name: name.value, message: message.value });
|
||||
name.value = '';
|
||||
message.value = '';
|
||||
emit('new-message', { text: text.value });
|
||||
text.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,26 +26,22 @@ function submit() {
|
||||
<fieldset class="ts-fieldset">
|
||||
<legend class="ts-legend">New Message</legend>
|
||||
<div class="ts-wrap is-vertical">
|
||||
<div class="ts-control">
|
||||
<div class="label">Name</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input">
|
||||
<input name="name" type="text" placeholder="Name" v-model="name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-control">
|
||||
<div class="label">Message</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input">
|
||||
<textarea name="message" placeholder="Message" v-model="message"></textarea>
|
||||
<textarea v-if="!locked" name="message" placeholder="Message" v-model="text" :maxlength="maxLength" rows="5"></textarea>
|
||||
<textarea v-else placeholder="Please log in to leave message" :maxlength="maxLength" rows="5" disabled></textarea>
|
||||
</div>
|
||||
<div class="ts-text is-small is-secondary is-end-aligned">
|
||||
{{ remainingCharacters }} / {{ maxLength }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-wrap has-top-spaced is-end-aligned">
|
||||
<button class="ts-button" type="submit" :class="{'is-disabled': name === '' || message === '' }">Submit</button>
|
||||
<button class="ts-button" type="submit" :class="{'is-disabled': text === '' || props.locked }">Submit</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
<script setup>
|
||||
defineProps(['message']);
|
||||
import { defineProps, defineEmits, computed } from 'vue';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const props = defineProps(['message']);
|
||||
const emit = defineEmits(['delete-message']);
|
||||
|
||||
const userId = authStore.id;
|
||||
|
||||
const onDelete = () => {
|
||||
emit('delete-message', props.message.id);
|
||||
};
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
return props.message?.avatar ? import.meta.env.VITE_R2_BASE_URL + props.message?.avatar : '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ts-conversation">
|
||||
<div class="avatar">
|
||||
<img src="../../assets/user.png" alt="Avatar" />
|
||||
<img :src="avatarUrl" alt="Avatar" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="bubble">
|
||||
<div class="author">{{ message.name }}</div>
|
||||
<div class="text">{{ message.message }}</div>
|
||||
<div class="ts-grid">
|
||||
<div class="column is-fluid">
|
||||
<div class="author">{{ message.username }}</div>
|
||||
<div class="text">{{ message.message }}</div>
|
||||
<div class="meta">
|
||||
<div class="item">{{ new Date(message.timestamp).toLocaleString('zh-TW', {timeZone: "-08:00"}) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column" v-if="userId === message.userId">
|
||||
<button class="ts-button is-icon is-negative is-outlined" @click="onDelete">
|
||||
<span class="ts-icon is-trash-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,4 +48,8 @@ defineProps(['message']);
|
||||
.ts-conversation > .content {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
.ts-conversation .bubble .text {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emit = defineEmits(['new-user']);
|
||||
|
||||
const name = defineModel();
|
||||
|
||||
function submit() {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('new-user', { name: name.value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<fieldset class="ts-fieldset">
|
||||
<legend class="ts-legend">New User</legend>
|
||||
<div class="ts-wrap is-vertical">
|
||||
<div class="ts-control">
|
||||
<div class="label">Name</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input">
|
||||
<input name="name" type="text" placeholder="Name" v-model="name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-wrap has-top-spaced is-end-aligned">
|
||||
<button class="ts-button" type="submit" :class="{'is-disabled': name === '' }">Submit</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { login } from '../../lib/api';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const router = useRouter();
|
||||
|
||||
const usernameError = ref('');
|
||||
const passwordError = ref('');
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
function validateUsername() {
|
||||
if (!username.value) {
|
||||
usernameError.value = 'Username is required.';
|
||||
} else if (username.value.length < 3) {
|
||||
usernameError.value = 'Username must be at least 3 characters.';
|
||||
} else {
|
||||
usernameError.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function validatePassword() {
|
||||
if (!password.value) {
|
||||
passwordError.value = 'Password is required.';
|
||||
} else if (password.value.length < 8) {
|
||||
passwordError.value = 'Password must be at least 8 characters.';
|
||||
} else {
|
||||
passwordError.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => username.value,
|
||||
() => {
|
||||
validateUsername();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => password.value,
|
||||
() => {
|
||||
validatePassword();
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = async () => {
|
||||
validateUsername();
|
||||
validatePassword();
|
||||
|
||||
if (usernameError.value || passwordError.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await login(username.value, password.value);
|
||||
const { jwt } = response;
|
||||
authStore.setJwt(jwt);
|
||||
alert('Login successful!');
|
||||
router.push('/profile');
|
||||
} catch (error) {
|
||||
alert("Login failed: " + error.message);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<fieldset class="ts-fieldset">
|
||||
<legend class="ts-legend">Login</legend>
|
||||
<div class="ts-wrap is-vertical">
|
||||
<div class="ts-control">
|
||||
<div class="label">Username</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input" :class="{'is-negative': usernameError}">
|
||||
<input name="username" type="text" placeholder="Username" v-model="username" />
|
||||
</div>
|
||||
<div class="ts-text is-small is-negative" v-if="usernameError">{{ usernameError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-control">
|
||||
<div class="label">Password</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input" :class="{'is-negative': passwordError}">
|
||||
<input name="password" type="password" placeholder="Password" v-model="password" />
|
||||
</div>
|
||||
<div class="ts-text is-small is-negative" v-if="passwordError">{{ passwordError }}</div>
|
||||
<div class="ts-text is-small is-negative">Warning: This is for demonstration purposes only. Passwords are stored in plaintext!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-wrap has-top-spaced is-end-aligned">
|
||||
<button class="ts-button" type="submit" :class="{
|
||||
'is-disabled': username === '' || password === '' || usernameError !== '' || passwordError !== ''
|
||||
}">Submit</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ts-error {
|
||||
color: red;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,25 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useRouter, RouterLink } from 'vue-router';
|
||||
import { computed } from 'vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const isLoggedIn = computed(() => {
|
||||
return authStore.isLoggedIn;
|
||||
});
|
||||
|
||||
const username = computed(() => {
|
||||
return authStore.username;
|
||||
});
|
||||
|
||||
function logout() {
|
||||
authStore.clearJwt();
|
||||
alert('登出成功!');
|
||||
// Redirect to the login page
|
||||
router.push('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -8,11 +28,19 @@ import { RouterLink } from 'vue-router';
|
||||
<div class="ts-wrap">
|
||||
<RouterLink class="ts-header is-brand" to="/">網路攻防實習</RouterLink>
|
||||
<div class="ts-tab is-tall">
|
||||
<RouterLink class="item" :to="route.path" :class="{'is-active': $route.path == route.path}" v-for="route in $router.options.routes.filter(route => route.meta.showInNav)">{{ route.meta.navName }}</RouterLink>
|
||||
<RouterLink class="item" :to="route.path" :class="{'is-active': route.path == $route.path}" v-for="route in $router.options.routes.filter(route => route.meta.showInNav)">{{ route.meta.navName }}</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ts-button is-disabled" disabled>登入</button>
|
||||
<div class="actions ts-wrap">
|
||||
<template v-if="isLoggedIn">
|
||||
<span class="username">Hi, {{ username }}</span>
|
||||
<RouterLink class="ts-button" to="/profile">個人資料</RouterLink>
|
||||
<div class="ts-button" @click="logout">登出</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<RouterLink class="ts-button" to="/login">登入</RouterLink>
|
||||
<RouterLink class="ts-button" to="/register">註冊</RouterLink>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -33,4 +61,9 @@ nav {
|
||||
.ts-header.is-brand {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
.username {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup>
|
||||
import { ref, computed, defineProps } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { uploadAvatar } from '../../lib/api';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
|
||||
const props = defineProps({
|
||||
profile: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const jwt = authStore.jwt;
|
||||
|
||||
const avatarFile = ref(null);
|
||||
const avatarError = ref('');
|
||||
|
||||
const onFileChange = (event) => {
|
||||
avatarFile.value = event.target.files[0];
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
avatarError.value = '';
|
||||
|
||||
if (!avatarFile.value) {
|
||||
avatarError.value = 'Avatar is required.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (avatarFile.value.size > 2 * 1024 * 1024) {
|
||||
avatarError.value = 'Avatar must be less than 2MB.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (avatarFile.value.type !== 'image/jpeg' && avatarFile.value.type !== 'image/png') {
|
||||
avatarError.value = 'Avatar must be a JPG or PNG image.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await uploadAvatar(avatarFile.value, jwt);
|
||||
alert('Avatar uploaded successfully!');
|
||||
// After successful upload, reload this page
|
||||
router.go(0);
|
||||
} catch (error) {
|
||||
alert("Avatar upload failed: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
if (avatarFile.value) {
|
||||
return URL.createObjectURL(avatarFile.value);
|
||||
}
|
||||
|
||||
return props.profile?.avatar ? import.meta.env.VITE_R2_BASE_URL + props.profile.avatar : '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<fieldset class="ts-fieldset">
|
||||
<legend class="ts-legend">Update Profile</legend>
|
||||
<div class="ts-wrap is-vertical">
|
||||
<div class="ts-control">
|
||||
<div class="label">Username</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input">
|
||||
<input type="text" name="username" placeholder="Username" :value="props.profile?.username" readonly disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-control">
|
||||
<div class="label">Avatar</div>
|
||||
<div class="content is-fluid">
|
||||
<div v-if="avatarUrl">
|
||||
<img :src="avatarUrl" alt="Avatar" style="max-width: 100px; max-height: 100px;" />
|
||||
</div>
|
||||
<div class="ts-file" :class="{'is-negative': avatarError}">
|
||||
<input type="file" accept="image/jpeg, image/png" @change="onFileChange" />
|
||||
</div>
|
||||
<div class="ts-text is-small is-negative" v-if="avatarError">{{ avatarError }}</div>
|
||||
<div class="ts-text is-small">Avatar must be a JPG or PNG image less than 2MB.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-wrap has-top-spaced is-end-aligned">
|
||||
<button class="ts-button" type="submit">Update</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ts-error {
|
||||
color: red;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const emit = defineEmits(['new-user']);
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
|
||||
const usernameError = ref('');
|
||||
const passwordError = ref('');
|
||||
|
||||
function validateUsername() {
|
||||
if (!username.value) {
|
||||
usernameError.value = 'Username is required.';
|
||||
} else if (username.value.length < 3) {
|
||||
usernameError.value = 'Username must be at least 3 characters.';
|
||||
} else {
|
||||
usernameError.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function validatePassword() {
|
||||
if (!password.value) {
|
||||
passwordError.value = 'Password is required.';
|
||||
} else if (password.value.length < 8) {
|
||||
passwordError.value = 'Password must be at least 8 characters.';
|
||||
} else {
|
||||
passwordError.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => username.value,
|
||||
() => {
|
||||
validateUsername();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => password.value,
|
||||
() => {
|
||||
validatePassword();
|
||||
}
|
||||
);
|
||||
|
||||
const submit = () => {
|
||||
validateUsername();
|
||||
validatePassword();
|
||||
|
||||
if (usernameError.value || passwordError.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('new-user', { username: username.value, password: password.value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<fieldset class="ts-fieldset">
|
||||
<legend class="ts-legend">New User</legend>
|
||||
<div class="ts-wrap is-vertical">
|
||||
<div class="ts-control">
|
||||
<div class="label">Username</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input" :class="{'is-negative': usernameError}">
|
||||
<input name="username" type="text" placeholder="Username" v-model="username" @input="validateUsername" />
|
||||
</div>
|
||||
<div class="ts-text is-small is-negative" v-if="usernameError">{{ usernameError }}</div>
|
||||
<div class="ts-text is-small">Username must be alphanumeric and at least 3 characters.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-control">
|
||||
<div class="label">Password</div>
|
||||
<div class="content is-fluid">
|
||||
<div class="ts-input" :class="{'is-negative': passwordError}">
|
||||
<input name="password" type="password" placeholder="Password" v-model="password" @input="validatePassword" />
|
||||
</div>
|
||||
<div class="ts-text is-small is-negative" v-if="passwordError">{{ passwordError }}</div>
|
||||
<div class="ts-text is-small">Password must be at least 8 characters.</div>
|
||||
<div class="ts-text is-small is-negative">Warning: This is for demonstration purposes only. Passwords are stored in plaintext!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-wrap has-top-spaced is-end-aligned">
|
||||
<button class="ts-button" type="submit" :class="{'is-disabled': username === '' || password === '' }">Submit</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ts-error {
|
||||
color: red;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import { unauthRedirectToLogin } from '../router';
|
||||
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
|
||||
export async function register(username, password) {
|
||||
const response = await fetch(API_BASE_URL + "/register", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Registration failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function login(username, password) {
|
||||
const response = await fetch(API_BASE_URL + "/login", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Login failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function postMessage(message, jwt) {
|
||||
const response = await fetch(API_BASE_URL + "/messages", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + jwt,
|
||||
},
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
unauthRedirectToLogin();
|
||||
return;
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Posting message failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteMessage(messageId, jwt) {
|
||||
const response = await fetch(API_BASE_URL + "/messages", {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + jwt,
|
||||
},
|
||||
body: JSON.stringify({ messageId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
unauthRedirectToLogin();
|
||||
return;
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Deleting message failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getMessages(jwt) {
|
||||
const response = await fetch(API_BASE_URL + "/messages", {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
unauthRedirectToLogin();
|
||||
return;
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Getting messages failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getProfile(jwt) {
|
||||
const response = await fetch(API_BASE_URL + "/me", {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + jwt,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
unauthRedirectToLogin();
|
||||
return;
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Getting profile failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function me(jwt) {
|
||||
const response = await fetch(API_BASE_URL + "/me", {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + jwt,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
unauthRedirectToLogin();
|
||||
return;
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'User operation failed');
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
const avatarFilename = userData.avatar;
|
||||
|
||||
if (avatarFilename) {
|
||||
return import.meta.env.VITE_R2_BASE_URL + "/" + avatarFilename;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function uploadAvatar(avatar, jwt) {
|
||||
const body = new FormData();
|
||||
body.append('avatar', avatar);
|
||||
|
||||
const response = await fetch(API_BASE_URL + "/avatars", {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + jwt,
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
unauthRedirectToLogin();
|
||||
return;
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Avatar upload failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
@@ -2,8 +2,11 @@ import { createApp } from 'vue';
|
||||
import './style.css';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
|
||||
+31
-14
@@ -1,9 +1,11 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import HomeView from '../views/HomeView.vue';
|
||||
import BoardView from '../views/BoardView.vue';
|
||||
import AboutView from '../views/AboutView.vue';
|
||||
import UsersView from '../views/UsersView.vue';
|
||||
import CreateUserView from '../views/CreateUserView.vue';
|
||||
import LoginView from '../views/LoginView.vue';
|
||||
import ProfileView from '../views/ProfileView.vue';
|
||||
import RegisterView from '../views/RegisterView.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -35,22 +37,30 @@ const routes = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: UsersView,
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: LoginView,
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
showInNav: true,
|
||||
navName: '使用者列表'
|
||||
showInNav: false,
|
||||
navName: '登入'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users/create',
|
||||
name: 'CreateUser',
|
||||
component: CreateUserView,
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: RegisterView,
|
||||
meta: {
|
||||
showInNav: true,
|
||||
navName: '新增使用者'
|
||||
showInNav: false,
|
||||
navName: '註冊'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: ProfileView,
|
||||
meta: {
|
||||
showInNav: false,
|
||||
navName: '個人資料'
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -60,4 +70,11 @@ const router = createRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
export default router;
|
||||
export function unauthRedirectToLogin() {
|
||||
const auth = useAuthStore();
|
||||
auth.clearJwt();
|
||||
alert('Your session has expired. Redirecting to login.');
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
jwt: localStorage.getItem('jwt') || null,
|
||||
}),
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.jwt,
|
||||
id: (state) => {
|
||||
if (state.jwt) {
|
||||
try {
|
||||
const payload = JSON.parse(atob(state.jwt.split('.')[1]));
|
||||
return payload.id;
|
||||
} catch (error) {
|
||||
console.error("Failed to decode JWT:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
username: (state) => {
|
||||
if (state.jwt) {
|
||||
try {
|
||||
const payload = JSON.parse(atob(state.jwt.split('.')[1]));
|
||||
return payload.username;
|
||||
} catch (error) {
|
||||
console.error("Failed to decode JWT:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
avatar: (state) => {
|
||||
if (state.jwt) {
|
||||
try {
|
||||
const payload = JSON.parse(atob(state.jwt.split('.')[1]));
|
||||
return payload.avatar;
|
||||
} catch (error) {
|
||||
console.error("Failed to decode JWT:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setJwt(newJwt) {
|
||||
this.jwt = newJwt;
|
||||
localStorage.setItem('jwt', newJwt);
|
||||
},
|
||||
clearJwt() {
|
||||
this.jwt = null;
|
||||
localStorage.removeItem('jwt');
|
||||
},
|
||||
},
|
||||
});
|
||||
+41
-6
@@ -1,22 +1,57 @@
|
||||
<script setup>
|
||||
import BoardForm from '../components/Board/BoardForm.vue';
|
||||
import BoardMessage from '../components/Board/BoardMessage.vue';
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { postMessage, deleteMessage, getMessages } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
|
||||
const messages = ref([]);
|
||||
|
||||
const onSubmit = (message) => {
|
||||
messages.value.push(message);
|
||||
const authStore = useAuthStore();
|
||||
const jwt = authStore.jwt;
|
||||
const isLoggedIn = authStore.isLoggedIn;
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await getMessages(jwt);
|
||||
messages.value = response.messages;
|
||||
} catch (error) {
|
||||
alert("Failed to get messages: " + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = async (message) => {
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
try {
|
||||
await postMessage(message.text, jwt);
|
||||
|
||||
const response = await getMessages();
|
||||
messages.value = response.messages;
|
||||
} catch (error) {
|
||||
alert("Failed to post message: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (messageId) => {
|
||||
try {
|
||||
await deleteMessage(messageId, jwt);
|
||||
|
||||
const response = await getMessages();
|
||||
messages.value = response.messages;
|
||||
} catch (error) {
|
||||
alert("Failed to delete message: " + error.message);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ts-container">
|
||||
<BoardForm @new-message="onSubmit" />
|
||||
<BoardForm :locked="!isLoggedIn" @new-message="onSubmit" />
|
||||
<div class="ts-content is-horizontally-fitted is-vertically-padded">
|
||||
<div class="ts-wrap is-vertical">
|
||||
<BoardMessage :message="message" v-for="message in messages" :key="message.id" />
|
||||
<BoardMessage :message="message" v-for="message in messages" :key="message.id" @delete-message="onDelete(message.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<script setup>
|
||||
import CreateForm from '../components/CreateUser/CreateForm.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const name = ref('');
|
||||
|
||||
const onSubmit = async () => {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name.value,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
alert('Failed to create user');
|
||||
return;
|
||||
}
|
||||
|
||||
alert('User created');
|
||||
name.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ts-container">
|
||||
<CreateForm v-model="name" @new-user="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="ts-container">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LoginForm from '../components/Login/LoginForm.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const response = await login(username.value, password.value);
|
||||
const { jwt } = response;
|
||||
localStorage.setItem('jwt', jwt);
|
||||
alert('Login successful!');
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
alert("Login failed: " + error.message);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="ts-container">
|
||||
<ProfileForm :profile="profile" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProfileForm from '../components/Profile/ProfileForm.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { getProfile } from '../lib/api';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const profile = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await getProfile(authStore.jwt);
|
||||
profile.value = data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch profile:", error);
|
||||
alert("Failed to fetch profile: " + error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import RegisterForm from '../components/Register/RegisterForm.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { register } from '../lib/api';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleNewUser = async ({ username, password }) => {
|
||||
try {
|
||||
const response = await register(username, password);
|
||||
|
||||
alert(response.message || 'Registration successful! Please log in.');
|
||||
// Redirect to login page
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
alert("Registration error: " + error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ts-container">
|
||||
<RegisterForm @new-user="handleNewUser" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script setup>
|
||||
import { onActivated, ref } from 'vue';
|
||||
|
||||
const users = ref([]);
|
||||
|
||||
onActivated(async () => {
|
||||
const response = await fetch('/api/users');
|
||||
|
||||
if (!response.ok) {
|
||||
users.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
users.value = data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ts-container">
|
||||
<pre>{{ users }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
+11
-1
@@ -1,7 +1,17 @@
|
||||
name = "ntu-awd-website"
|
||||
pages_build_output_dir = "dist"
|
||||
compatibility_date = "2025-04-12"
|
||||
|
||||
[vars]
|
||||
hcaptcha_site_key = "a7340f48-b55e-4c56-8d96-2e70ce3423e0"
|
||||
hcaptcha_secret_key = "ES_8b04993dc0004f59864d11bb1dc6a3bc"
|
||||
JWT_SECRET = "7KH0adxP9mYrUYrEs0p_ccecRiaQf9IuxalS5r10QVI"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "awd-db"
|
||||
database_id = "a2088769-aab4-44be-b24e-25c8762f0e80"
|
||||
database_id = "a2088769-aab4-44be-b24e-25c8762f0e80"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "MY_BUCKET"
|
||||
bucket_name = "ntu-padn-mid-web"
|
||||
Reference in New Issue
Block a user