From fb916e9e9f1ee0c95f24b6efd19600a5b29a978a Mon Sep 17 00:00:00 2001 From: Lynixenn Date: Sun, 30 Mar 2025 22:02:56 +0200 Subject: [PATCH] Merged API and Databank into Main branch --- api/doc/API.md | 499 ++++++++++++++++++++++++ api/helpers.ts | 73 ++++ api/helpers/chat_api.ts | 142 +++++++ api/helpers/comments_api.ts | 142 +++++++ api/helpers/mod.ts | 11 + api/helpers/post_api.ts | 147 +++++++ api/helpers/user_api.ts | 45 +++ api/main.ts | 307 +++++++++++++++ database/create_db.ts | 107 +++++ database/doc/DATABASE_UTILS.md | 0 database/helpers/interfaces.ts | 61 +++ database/helpers/maphelper.ts | 122 ++++++ database/helpers/utils/chat_utils.ts | 86 ++++ database/helpers/utils/comment_utils.ts | 81 ++++ database/helpers/utils/core_utils.ts | 46 +++ database/helpers/utils/mod.ts | 13 + database/helpers/utils/post_utils.ts | 128 ++++++ database/helpers/utils/user_utils.ts | 103 +++++ database/utils.ts | 203 ++++++++++ 19 files changed, 2316 insertions(+) create mode 100644 api/doc/API.md create mode 100644 api/helpers.ts create mode 100644 api/helpers/chat_api.ts create mode 100644 api/helpers/comments_api.ts create mode 100644 api/helpers/mod.ts create mode 100644 api/helpers/post_api.ts create mode 100644 api/helpers/user_api.ts create mode 100644 api/main.ts create mode 100644 database/create_db.ts create mode 100644 database/doc/DATABASE_UTILS.md create mode 100644 database/helpers/interfaces.ts create mode 100644 database/helpers/maphelper.ts create mode 100644 database/helpers/utils/chat_utils.ts create mode 100644 database/helpers/utils/comment_utils.ts create mode 100644 database/helpers/utils/core_utils.ts create mode 100644 database/helpers/utils/mod.ts create mode 100644 database/helpers/utils/post_utils.ts create mode 100644 database/helpers/utils/user_utils.ts create mode 100644 database/utils.ts diff --git a/api/doc/API.md b/api/doc/API.md new file mode 100644 index 0000000..316023f --- /dev/null +++ b/api/doc/API.md @@ -0,0 +1,499 @@ +# This was generated using AI!!! Will edit if errors are found. + + +# API Documentation + +## Index + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/` | Returns basic information and directions for using the API endpoints. | +| GET | `/api` | Redirects to the full API documentation. | + +--- + +## Account Endpoints + +| Method | Endpoint | Description | +|--------|-------------------------------------------|----------------------------------------------------------------| +| POST | `/api/account/login` | Logs in a user with username and password. | +| POST | `/api/account/register` | Registers a new user with required details. | +| POST | `/api/account/logout` | Logs a user out. (TODO) | +| POST | `/api/account/password/forgot` | Initiates password recovery. (TODO) | +| POST | `/api/account/password/reset` | Resets the password. (TODO) | +| POST | `/api/account/password/change` | Changes the password. (TODO) | +| POST | `/api/account/email/change-email` | Changes the user's email address. (TODO) | +| POST | `/api/account/email/verify-email` | Verifies a user's email. (TODO) | +| POST | `/api/account/delete-account` | Deletes the user account. (TODO) | +| POST | `/api/account/block` | Blocks a user account. (TODO) | + +--- + +## Auth Endpoints + +| Method | Endpoint | Description | +|--------|----------------------|--------------------------------------------------| +| GET | `/api/auth` | Auth endpoint (placeholder). | +| GET | `/api/auth/verify` | Verifies an authentication token. (TODO) | +| GET | `/api/auth/refresh` | Refreshes an authentication token. (TODO) | + +--- + +## User Endpoints + +| Method | Endpoint | Description | +|--------|---------------|------------------------------------------------------------| +| GET | `/api/users` | Retrieves a list of all users from the database. | + +--- + +## Chat Endpoints + +| Method | Endpoint | Description | +|--------|----------------------------------|-----------------------------------------------------------------| +| GET | `/api/chats` | Retrieves chats for a user (requires an Authorization header). | +| GET | `/api/chat/:id` | Retrieves details of a specific chat using its ID. | +| POST | `/api/chat/create` | Creates a new chat with at least 2 participants. | +| POST | `/api/chat/:id/message` | Sends a message to the specified chat. | +| DELETE | `/api/chat/:id` | Deletes a specific chat by its ID. | + +--- + +## Post Endpoints + +| Method | Endpoint | Description | +|--------|----------------------------------|-------------------------------------------------------------------| +| GET | `/api/posts` | Retrieves all posts. | +| GET | `/api/post/:id` | Retrieves a specific post by its ID. | +| POST | `/api/post/create` | Creates a new post. | +| PUT | `/api/post/:id` | Updates an existing post (requires at least one update field). | +| DELETE | `/api/post/:id` | Deletes a specific post by its ID. | +| POST | `/api/post/:id/like` | Likes a specific post. | + +--- + +## Comment Endpoints + +| Method | Endpoint | Description | +|--------|-----------------------------------|--------------------------------------------------------------------| +| GET | `/api/post/:id/comments` | Retrieves all comments associated with a specific post. | +| POST | `/api/post/:id/comment` | Adds a new comment to a specified post. | +| PUT | `/api/comment/:id` | Updates an existing comment using its ID. | +| DELETE | `/api/comment/:id` | Deletes a specific comment by its ID. | +| POST | `/api/comment/:id/like` | Likes a specific comment. | + +> Note: Replace any `:id` placeholder in the URLs with the actual identifier when making requests. +> Note: Endpoints marked as TODO are placeholders for future implementation. + +--- + +## Base URL + +Assuming the server is running on port 8000, the base URL is: +``` +http://localhost:8000 +``` + +--- + +## Index & Documentation Routes + +- **GET /** + A simple informational endpoint. + - **Response:** `"For endpoints, use /api/{name}"` + + **Example:** + ```bash + curl http://localhost:8000/ + ``` + +- **GET /api** + Directs users for API documentation. + - **Response:** `"For API Documentation, visit /docs"` + + **Example:** + ```bash + curl http://localhost:8000/api + ``` + +--- + +## Account Endpoints + +### 1. POST /api/account/login + +Log in a user with their username and password. + +- **Request Body:** + ```json + { + "username": "johndoe", + "password": "secret123" + } + ``` +- **Response:** On success returns status 200 with body `"Success"`. If unsuccessful, appropriate error messages are returned. + +**Example:** +```bash +curl -X POST http://localhost:8000/api/account/login \ + -H "Content-Type: application/json" \ + -d '{"username": "johndoe", "password": "secret123"}' +``` + +### 2. POST /api/account/register + +Register a new user with all required fields. + +- **Request Body:** + ```json + { + "username": "johndoe", + "password": "secret123", + "userGroup": "default", + "displayname": "John Doe", + "user_email": "john@example.com", + "firstname": "John", + "surname": "Doe" + } + ``` +- **Response:** On success returns a status of 200 and a message indicating the user ID registration. + +**Example:** +```bash +curl -X POST http://localhost:8000/api/account/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "johndoe", + "password": "secret123", + "userGroup": "default", + "displayname": "John Doe", + "user_email": "john@example.com", + "firstname": "John", + "surname": "Doe" + }' +``` + +### Other Account Endpoints + +- POST `/api/account/logout` +- POST `/api/account/password/forgot` +- POST `/api/account/password/reset` +- POST `/api/account/password/change` +- POST `/api/account/email/change-email` +- POST `/api/account/email/verify-email` +- POST `/api/account/delete-account` +- POST `/api/account/block` + +These endpoints are marked as TODO and currently have no implemented logic. + +--- + +## Auth Endpoints + +- GET `/api/auth` +- GET `/api/auth/verify` +- GET `/api/auth/refresh` + +These endpoints are also marked as TODO. + +--- + +## User Endpoints + +### GET /api/users + +Retrieve all users from the database. + +- **Response:** JSON array of user objects. + +**Example:** +```bash +curl http://localhost:8000/api/users +``` + +--- + +## Chat Endpoints + +### 1. GET /api/chats + +Retrieve all chats for a user. Requires an "Authorization" header and an optional query parameter for userId. + +- **Headers:** + - Authorization: Bearer (only required to simulate authentication) +- **Query Parameter:** + - userId (optional; defaults to "1" if not provided) +- **Response:** JSON array of chats for the specified user. + +**Example:** +```bash +curl "http://localhost:8000/api/chats?userId=2" \ + -H "Authorization: Bearer mytoken123" +``` + +### 2. GET /api/chat/:id + +Retrieve details of a specific chat identified by its ID. + +- **Route Parameter:** + - id: Chat ID +- **Response:** JSON object containing chat details. + +**Example:** +```bash +curl http://localhost:8000/api/chat/10 +``` + +### 3. POST /api/chat/create + +Create a new chat with at least two participants. + +- **Request Body:** + ```json + { + "participants": [1, 2], + "chatName": "Group Chat" + } + ``` +- **Response:** Returns status 201 and a JSON object with the newly created "chatId". + +**Example:** +```bash +curl -X POST http://localhost:8000/api/chat/create \ + -H "Content-Type: application/json" \ + -d '{"participants": [1, 2], "chatName": "Group Chat"}' +``` + +### 4. POST /api/chat/:id/message + +Send a message to a specific chat. + +- **Route Parameter:** + - id: Chat ID +- **Request Body:** + ```json + { + "senderId": 1, + "content": "Hello, world!" + } + ``` +- **Response:** Returns status 201 and a JSON object with the "messageId" of the new message. + +**Example:** +```bash +curl -X POST http://localhost:8000/api/chat/10/message \ + -H "Content-Type: application/json" \ + -d '{"senderId": 1, "content": "Hello, world!"}' +``` + +### 5. DELETE /api/chat/:id + +Delete a specific chat. + +- **Route Parameter:** + - id: Chat ID +- **Response:** Returns status 200 with a confirmation message. + +**Example:** +```bash +curl -X DELETE http://localhost:8000/api/chat/10 +``` + +--- + +## Post Endpoints + +### 1. GET /api/posts + +Retrieve all posts. + +- **Response:** JSON array of posts. + +**Example:** +```bash +curl http://localhost:8000/api/posts +``` + +### 2. GET /api/post/:id + +Retrieve a specific post by its ID. + +- **Route Parameter:** + - id: Post ID +- **Response:** JSON object representing the post. + +**Example:** +```bash +curl http://localhost:8000/api/post/15 +``` + +### 3. POST /api/post/create + +Create a new post. + +- **Request Body:** + ```json + { + "userId": 1, + "postText": "This is a new post", + "postType": "text" + } + ``` +- **Response:** Returns status 201 with the "postId" of the created post. + +**Example:** +```bash +curl -X POST http://localhost:8000/api/post/create \ + -H "Content-Type: application/json" \ + -d '{"userId": 1, "postText": "This is a new post", "postType": "text"}' +``` + +### 4. PUT /api/post/:id + +Update an existing post. At least one of `postText` or `postType` must be provided. + +- **Route Parameter:** + - id: Post ID +- **Request Body:** + ```json + { + "postText": "Updated post text", + "postType": "text" + } + ``` +- **Response:** Returns status 200 with a confirmation message. + +**Example:** +```bash +curl -X PUT http://localhost:8000/api/post/15 \ + -H "Content-Type: application/json" \ + -d '{"postText": "Updated post text", "postType": "text"}' +``` + +### 5. DELETE /api/post/:id + +Delete a specific post. + +- **Route Parameter:** + - id: Post ID +- **Response:** Returns status 200 with a message confirming deletion. + +**Example:** +```bash +curl -X DELETE http://localhost:8000/api/post/15 +``` + +### 6. POST /api/post/:id/like + +Like a specific post. + +- **Route Parameter:** + - id: Post ID +- **Request Body:** + ```json + { + "userId": 1 + } + ``` +- **Response:** Returns status 200 with a confirmation message. + +**Example:** +```bash +curl -X POST http://localhost:8000/api/post/15/like \ + -H "Content-Type: application/json" \ + -d '{"userId": 1}' +``` + +--- + +## Comment Endpoints + +### 1. GET /api/post/:id/comments + +Retrieve all comments for a specific post. + +- **Route Parameter:** + - id: Post ID +- **Response:** JSON array of comments. + +**Example:** +```bash +curl http://localhost:8000/api/post/15/comments +``` + +### 2. POST /api/post/:id/comment + +Create a new comment on a specific post. + +- **Route Parameter:** + - id: Post ID +- **Request Body:** + ```json + { + "userId": 1, + "text": "This is a comment" + } + ``` +- **Response:** Returns status 201 with the newly created "commentId". + +**Example:** +```bash +curl -X POST http://localhost:8000/api/post/15/comment \ + -H "Content-Type: application/json" \ + -d '{"userId": 1, "text": "This is a comment"}' +``` + +### 3. PUT /api/comment/:id + +Update an existing comment. + +- **Route Parameter:** + - id: Comment ID +- **Request Body:** + ```json + { + "text": "Updated comment text" + } + ``` +- **Response:** Returns status 200 with a confirmation message. + +**Example:** +```bash +curl -X PUT http://localhost:8000/api/comment/20 \ + -H "Content-Type: application/json" \ + -d '{"text": "Updated comment text"}' +``` + +### 4. DELETE /api/comment/:id + +Delete a specific comment. + +- **Route Parameter:** + - id: Comment ID +- **Response:** Returns status 200 with a confirmation message. + +**Example:** +```bash +curl -X DELETE http://localhost:8000/api/comment/20 +``` + +### 5. POST /api/comment/:id/like + +Like a specific comment. + +- **Route Parameter:** + - id: Comment ID +- **Request Body:** + ```json + { + "userId": 1 + } + ``` +- **Response:** Returns status 200 with a confirmation message. + +**Example:** +```bash +curl -X POST http://localhost:8000/api/comment/20/like \ + -H "Content-Type: application/json" \ + -d '{"userId": 1}' +``` \ No newline at end of file diff --git a/api/helpers.ts b/api/helpers.ts new file mode 100644 index 0000000..626613e --- /dev/null +++ b/api/helpers.ts @@ -0,0 +1,73 @@ +/// +/** + * @author Esad Mustafoski + * @file api/helpers.ts + * @description Helper functions for the API + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { Context } from "https://deno.land/x/oak/mod.ts"; +import { encodeHex } from "jsr:@std/encoding/hex"; +// import { hash } from "node:crypto"; + +export type ApiResponse = { + status: number; + body: unknown; +}; + + +// +++ FUNCTIONS ---------------------------------------------------- // +/** + * @description Sends a response to the client + * @usage sendResponse(ctx, { status: 200, body: { message: "Success" } }) + * Status is the HTTP Status code + * Body is the response body/message/data. + */ +const sendResponse = (ctx: Context, { status, body }: ApiResponse): void => { + ctx.response.status = status; + ctx.response.body = body as any; +}; + +/** + * @usage errorResponse(ctx, 401, "Unauthorized") + * @see sendResponse + */ +const errorResponse = (ctx: Context, status: number, message: string): void => { + sendResponse(ctx, { status, body: { error: message } }); +}; + +/** + * @description password "Salter", used to salt the passwords before the hash, this salt will be + * returned seperately to save the salt in the DB + * @param password The password to salt + * @returns {saltedPassword: string, salt: string} Password with the salt + Salt seperately, both strings + */ +const saltPassword = async ( + password: string, +): Promise<{ saltedPassword: string; salt: string }> => { + const saltBytes = new Uint8Array(16); // 16 bytes = 128 bits for randomness + await crypto.getRandomValues(saltBytes); + const salt = encodeHex(saltBytes); + + const saltedPassword = `${password}${salt}`; + + return { + saltedPassword, + salt, + }; +}; + +/** + * @description Hashing Function for Passwords/etc + * @param password The password to hash + * @returns {hash: string} The hashed password as a string + */ +const hashPassword = async (password: string): Promise => { + const to_hash = password; + const buffer = new TextEncoder().encode(to_hash); + const hash_buffer = await crypto.subtle.digest("SHA-256", buffer); + const hash = await encodeHex(hash_buffer); + return hash; +}; + +export { errorResponse, hashPassword, saltPassword, sendResponse }; diff --git a/api/helpers/chat_api.ts b/api/helpers/chat_api.ts new file mode 100644 index 0000000..414d241 --- /dev/null +++ b/api/helpers/chat_api.ts @@ -0,0 +1,142 @@ +/// +/** + * @author Esad Mustafoski + * @description API file for Comments + */ + +// +++ IMPORTS ------------------------------------------------------ // +import * as db_utils from "../../database/utils.ts"; +import * as helper_utils from "../helpers.ts"; + +// +++ FUNCTIONS ----------------------------------------------------- // +async function api_getChats(ctx: any): Promise { + try { + const authHeader = ctx.request.headers.get("Authorization"); + if (!authHeader) { + helper_utils.errorResponse(ctx, 401, "Authentication Required"); + return; + } + + // Get userId from query parameter (for testing) or use 1 as default (avoid errors because of userid) + // Assumes 1 is a test account + const userId = ctx.request.url.searchParams.get("userId") || "1"; + + const chats = await db_utils.getUserChats(userId); + ctx.response.body = chats; + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error retrieving chats"); + console.log(error); + } + } + +async function api_getChatById(ctx: any): Promise { + try { + const chatId = ctx.params.id; + if (!chatId) { + helper_utils.errorResponse(ctx, 400, "Chat ID required"); + return; + } + + const chat = await db_utils.getChatById(chatId); + if (!chat) { + helper_utils.errorResponse(ctx, 404, "Chat not found"); + return; + } + + ctx.response.body = chat; + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error retrieving chat"); + console.log(error); + } +} + +async function api_createChat(ctx: any): Promise { + try { + const body = ctx.request.body; + const result = await body.json(); + const { participants, chatName } = result; + + if ( + !participants || !Array.isArray(participants) || participants.length < 2 + ) { + helper_utils.errorResponse( + ctx, + 400, + "Two people required to create a chat", + ); + return; + } + + const chatId = await db_utils.createChat(participants, chatName || ""); + helper_utils.sendResponse(ctx, { + status: 201, + body: { chatId }, + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error creating chat"); + console.log(error); + } +} + +async function api_sendMessage(ctx: any): Promise { + try { + const chatId = ctx.params.id; + if (!chatId) { + helper_utils.errorResponse(ctx, 400, "Chat ID required"); + return; + } + + const body = ctx.request.body; + const result = await body.json(); + const { senderId, content } = result; + + if (!senderId || !content) { + helper_utils.errorResponse( + ctx, + 400, + "Sender ID and message content required", + ); + return; + } + + const messageId = await db_utils.addMessageToChat( + chatId, + senderId, + content, + ); + helper_utils.sendResponse(ctx, { + status: 201, + body: { messageId }, + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error sending message"); + console.log(error); + } +} + +async function api_deleteChat(ctx: any): Promise { + try { + const chatId = ctx.params.id; + if (!chatId) { + helper_utils.errorResponse(ctx, 400, "Chat ID required"); + return; + } + + await db_utils.deleteChat(chatId); + helper_utils.sendResponse(ctx, { + status: 200, + body: { message: "Chat deleted successfully" }, + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error deleting chat"); + console.log(error); + } +} + +export { + api_createChat, + api_deleteChat, + api_getChatById, + api_getChats, + api_sendMessage, +}; diff --git a/api/helpers/comments_api.ts b/api/helpers/comments_api.ts new file mode 100644 index 0000000..264e290 --- /dev/null +++ b/api/helpers/comments_api.ts @@ -0,0 +1,142 @@ +/// +/** + * @author Esad Mustafoski + * @description API file for Comments + */ + +// +++ IMPORTS ------------------------------------------------------ // +import * as db_utils from "../../database/utils.ts"; +import * as helper_utils from "../helpers.ts"; + +// +++ FUNCTIONS ----------------------------------------------------- // +async function api_getPostComments(ctx: any): Promise { + try { + const postId = ctx.params.id; + if (!postId) { + helper_utils.errorResponse(ctx, 400, "Post ID required"); + return; + } + + const comments = await db_utils.getCommentsFromDB(Number(postId)); + ctx.response.body = comments; + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error retrieving comments"); + console.log(error); + } +} + +async function api_createComment(ctx: any): Promise { + try { + const postId = ctx.params.id; + if (!postId) { + helper_utils.errorResponse(ctx, 400, "Post ID required"); + return; + } + + const body = ctx.request.body; + const result = await body.json(); + const { userId, text } = result; + + if (!userId || !text) { + helper_utils.errorResponse(ctx, 400, "User ID and comment text required"); + return; + } + + // Create timestamp in the format expected by the database + const createdAt = `${Math.floor(Date.now() / 1000)}-${ + new Date().toLocaleDateString("en-GB").split("/").join("-") + }`; + + const commentId = await db_utils.createComment(postId, userId, createdAt, text); + helper_utils.sendResponse(ctx, { + status: 201, + body: { commentId } + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error creating comment"); + console.log(error); + } +} + +async function api_updateComment(ctx: any): Promise { + try { + const commentId = ctx.params.id; + if (!commentId) { + helper_utils.errorResponse(ctx, 400, "Comment ID required"); + return; + } + + const body = ctx.request.body; + const result = await body.json(); + const { text } = result; + + if (!text) { + helper_utils.errorResponse(ctx, 400, "Comment text required"); + return; + } + + await db_utils.updateComment(commentId, text); + helper_utils.sendResponse(ctx, { + status: 200, + body: { message: "Comment updated successfully" } + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error updating comment"); + console.log(error); + } +} + +async function api_deleteComment(ctx: any): Promise { + try { + const commentId = ctx.params.id; + if (!commentId) { + helper_utils.errorResponse(ctx, 400, "Comment ID required"); + return; + } + + await db_utils.deleteComment(commentId); + helper_utils.sendResponse(ctx, { + status: 200, + body: { message: "Comment deleted successfully" } + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error deleting comment"); + console.log(error); + } +} + +async function api_likeComment(ctx: any): Promise { + try { + const commentId = ctx.params.id; + if (!commentId) { + helper_utils.errorResponse(ctx, 400, "Comment ID required"); + return; + } + + const body = ctx.request.body; + const result = await body.json(); + const { userId } = result; + + if (!userId) { + helper_utils.errorResponse(ctx, 400, "User ID required"); + return; + } + + await db_utils.likeComment(commentId, userId); + helper_utils.sendResponse(ctx, { + status: 200, + body: { message: "Comment liked successfully" } + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error liking comment"); + console.log(error); + } +} + +export { + api_createComment, + api_deleteComment, + api_getPostComments, + api_likeComment, + api_updateComment, +}; \ No newline at end of file diff --git a/api/helpers/mod.ts b/api/helpers/mod.ts new file mode 100644 index 0000000..5320818 --- /dev/null +++ b/api/helpers/mod.ts @@ -0,0 +1,11 @@ +/// +/** + * @author Esad Mustafoski + * @description A mod file is used to export all the functions in the folder, making them easier to access. + * @file mod.ts + */ + +export * from "./chat_api.ts"; +export * from "./comments_api.ts"; +export * from "./post_api.ts"; +export * from "./user_api.ts"; \ No newline at end of file diff --git a/api/helpers/post_api.ts b/api/helpers/post_api.ts new file mode 100644 index 0000000..7154b15 --- /dev/null +++ b/api/helpers/post_api.ts @@ -0,0 +1,147 @@ +/// +/** + * @author Esad Mustafoski + * @description API file for Posts + */ + +// +++ IMPORTS ------------------------------------------------------ // +import * as db_utils from "../../database/utils.ts"; +import * as helper_utils from "../helpers.ts"; +import { Context } from "https://deno.land/x/oak@v17.1.2/mod.ts"; + +// +++ FUNCTIONS ----------------------------------------------------- // +async function api_getPostById(ctx: any): Promise { + try { + const postId = ctx.params.id; + if (!postId) { + helper_utils.errorResponse(ctx, 400, "Post ID required"); + return; + } + + const post = await db_utils.getPostById(postId); + if (!post) { + helper_utils.errorResponse(ctx, 404, "Post not found"); + return; + } + + ctx.response.body = post; + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error retrieving post"); + } +} + +async function api_createPost(ctx: Context): Promise { + try { + const body = ctx.request.body; + const result = await body.json(); + const { userId, postText, postType } = result; + + if (!userId || !postText || !postType) { + helper_utils.errorResponse( + ctx, + 400, + "User ID, post text, and post type required", + ); + return; + } + + // Create timestamp in the format expected by the database + const createdAt = `${Math.floor(Date.now() / 1000)}-${new Date() + .toLocaleDateString("en-GB") + .split("/") + .join("-")}`; + + const postId = await db_utils.createPost( + userId, + createdAt, + postText, + postType, + ); + helper_utils.sendResponse(ctx, { + status: 201, + body: { postId }, + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error creating post"); + } +} + +async function api_updatePost(ctx: any): Promise { + try { + const postId = ctx.params.id; + if (!postId) { + helper_utils.errorResponse(ctx, 400, "Post ID required"); + return; + } + + const body = ctx.request.body; + const result = await body.json(); + const { postText, postType } = result; + + if (!postText && !postType) { + helper_utils.errorResponse(ctx, 400, "No update data provided"); + return; + } + + await db_utils.updatePost(postId, postText, postType); + helper_utils.sendResponse(ctx, { + status: 200, + body: { message: "Post updated successfully" }, + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error updating post"); + } +} + +async function api_deletePost(ctx: any): Promise { + try { + const postId = ctx.params.id; + if (!postId) { + helper_utils.errorResponse(ctx, 400, "Post ID required"); + return; + } + + await db_utils.deletePost(postId); + helper_utils.sendResponse(ctx, { + status: 200, + body: { message: "Post deleted successfully" }, + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error deleting post"); + } +} + +async function api_likePost(ctx: any): Promise { + try { + const postId = ctx.params.id; + if (!postId) { + helper_utils.errorResponse(ctx, 400, "Post ID required"); + return; + } + + const body = ctx.request.body; + const result = await body.json(); + const { userId } = result; + + if (!userId) { + helper_utils.errorResponse(ctx, 400, "User ID required"); + return; + } + + await db_utils.likePost(postId, userId); + helper_utils.sendResponse(ctx, { + status: 200, + body: { message: "Post liked successfully" }, + }); + } catch (error) { + helper_utils.errorResponse(ctx, 500, "Error liking post"); + } +} + +export { + api_createPost, + api_deletePost, + api_getPostById, + api_likePost, + api_updatePost, +}; diff --git a/api/helpers/user_api.ts b/api/helpers/user_api.ts new file mode 100644 index 0000000..265ac64 --- /dev/null +++ b/api/helpers/user_api.ts @@ -0,0 +1,45 @@ +/// +/** + * @author Esad Mustafoski + * @description API file for Users + */ + +// +++ IMPORTS ------------------------------------------------------ // +import * as db_utils from "../../database/utils.ts"; +// import * as helper_utils from "../helpers.ts"; +import { Context } from "https://deno.land/x/oak@v17.1.2/mod.ts"; + +// +++ FUNCTIONS ----------------------------------------------------- // + +/*async function api_user_getInfo(ctx: any): Promise { + const id = ctx.params.id; + + if (!id) { + ctx.response.status = 400; + ctx.response.body = { error: "User ID required" }; + return; + } + + try { + const user = await db_utils.getAllUserInfoByID(id); + if (user === null || user === undefined) { + helper_utils.errorResponse(ctx, 404, "User not found"); + return; + } + + ctx.response.body = user; + } catch (error) { + helper_utils.errorResponse(ctx, 500, error as string); + } +} +*/ + +async function api_getAllUsers(ctx: Context): Promise { + const getUsers = await db_utils.getAllUsersFromDB(); + ctx.response.body = getUsers; +} + +export { + api_getAllUsers, + // api_user_getInfo +}; diff --git a/api/main.ts b/api/main.ts new file mode 100644 index 0000000..b3a7409 --- /dev/null +++ b/api/main.ts @@ -0,0 +1,307 @@ +/// +/** + * @author Esad Mustafoski + * @description Main API file, Handles all the routing/api stuff + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { + Application, + Context, + Next, + Router, +} from "https://deno.land/x/oak@v17.1.2/mod.ts"; +import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts"; +import { + dirname, + fromFileUrl, + join, +} from "https://deno.land/std@0.224.0/path/mod.ts"; + +import * as db_utils from "../database/utils.ts"; +import * as helper_utils from "./helpers.ts"; + +import { + // --- Chat --- // + api_createChat, + api_createComment, + api_createPost, + api_deleteChat, + api_deleteComment, + api_deletePost, + // --- User --- // + api_getAllUsers, + api_getChatById, + api_getChats, + // --- Post --- // + api_getPostById, + // --- Comment --- // + api_getPostComments, + api_likeComment, + api_likePost, + api_sendMessage, + api_updateComment, + api_updatePost, +} from "./helpers/mod.ts"; + +// +++ VARIABLES / TYPES --------------------------------------------- // +const router = new Router(); +const app = new Application(); + +// unused for now +type ApiResponse = { + status: number; + body: unknown; +}; + +// database creation if missing, runs here because this is the main file executed by the API. +db_utils.ensureDatabaseExists(); + +// +++ ROUTER ------------------------------------------------------- // +// Creates the routes for the API server. +// Example: localhost:8000/api will show "testAPIPoint" +// in the HTML page. + +// Docs Routes +router + .get("/", (ctx: any) => { + ctx.response.body = "For endpoints, use /api/{name}"; + }) + .get("/api", (ctx: any) => { + ctx.response.body = "For API Documentation, visit /docs"; + }); + +// -- Account routes -- +router + .post("/api/account/login", api_login) + .post("/api/account/register", api_register) + .post("/api/account/logout", () => {}) // TODO + .post("/api/account/password/forgot", () => {}) // TODO + .post("/api/account/password/reset", () => {}) // TODO + .post("/api/account/password/change", () => {}) // TODO + .post("/api/account/email/change-email", () => {}) // TODO + .post("/api/account/email/verify-email", () => {}) // TODO + .post("/api/account/delete-account", () => {}) // TODO + .post("/api/account/block", () => {}); // TODO + +// -- Auth Routes -- // +router + .get("/api/auth", () => {}) // TODO + .get("/api/auth/verify", () => {}) // TODO + .get("/api/auth/refresh", () => {}); // TODO + +// -- User routes -- // +router.get("/api/users", api_getAllUsers); +// .get("/api/user/:id/info", api_user_getInfo); + +// -- Chat routes -- // +router + .get("/api/chats", api_getChats) + .get("/api/chat/:id", api_getChatById) + .post("/api/chat/create", api_createChat) + .post("/api/chat/:id/message", api_sendMessage) + .delete("/api/chat/:id", api_deleteChat); + +// -- Post routes -- // +router + .get("/api/posts", api_posts_getAll) + .get("/api/post/:id", api_getPostById) + .post("/api/post/create", api_createPost) + .put("/api/post/:id", api_updatePost) + .delete("/api/post/:id", api_deletePost) + .post("/api/post/:id/like", api_likePost); + +// -- Comment Routes -- // +router + .get("/api/post/:id/comments", api_getPostComments) + .post("/api/post/:id/comment", api_createComment) + .put("/api/comment/:id", api_updateComment) + .delete("/api/comment/:id", api_deleteComment) + .post("/api/comment/:id/like", api_likeComment); + +// +++ FUNCTIONS ----------------------------------------------------- // + +// ABANDONED FUNCTIONS // +async function _authenticator(ctx: Context, next: Next): Promise { + const authHeader = ctx.request.headers.get("Authorization"); + + if (!authHeader) { + ctx.response.status = 401; + ctx.response.body = { error: "No header" }; + return; + } + + // Bearer check + // Bearer is often used for authentication in API's and is a standard, I check it using RegEx (Regular Expressions) + const match = authHeader.match(/^Bearer (.+)$/); + if (!match) { + ctx.response.status = 401; + ctx.response.body = { error: "Invalid format" }; + return; + } + + const token = match[1]; + + try { + // Token logic missing, unattempted. + await next(); + } catch (error) { + ctx.response.status = 401; + ctx.response.body = { error: "Invalid token" }; + } +} + +async function _tokenChecker(ctx: Context, next: Next): Promise { + // logic below (TODO): + /** + * 1. check if the token is valid + * 2. if valid, set the user in the context + * 3. if not valid, return 401 + * 4. if: token missing, expired, blacklisted, !DB, !user or !correct user, return 401 with associated error + * eg: wrong user: 401 -> "Token not associated with this user" + */ +} + +// API: Posts // +async function api_posts_getAll(ctx: Context): Promise { + const posts = await db_utils.getPostsFromDB(); + ctx.response.body = posts; +} + +// API: login/register // +async function api_register(ctx: Context): Promise { + try { + const body = ctx.request.body; + const result = await body.json(); + const { + username, + password, + userGroup, + displayname, + user_email, + firstname, + surname, + } = result; + // Claude 3-5 Sonnet was used for the first Date formatting + const account_created = `${Math.floor(Date.now() / 1000)}-${ + new Date() + .toLocaleDateString("en-GB") + .split("/") + .join("-") + }`; + + if ( + !username || + !password || + !userGroup || + !displayname || + !user_email || + !firstname || + !surname + ) { + helper_utils.errorResponse(ctx, 400, "Missing required fields"); + return; + } + + // First salt the password + const { saltedPassword, salt } = await helper_utils.saltPassword(password); + // Then hash the salted password + const hash = await helper_utils.hashPassword(saltedPassword); + + const userId = db_utils.registerUser( + username, + hash, + salt, + userGroup, + displayname, + user_email, + firstname, + surname, + account_created, + ); + + const user = await db_utils.getUserByUsername(username); + + const responseBody: any = { + success: true, + message: "Register successful", + }; + + if (user.user_id !== undefined) { + responseBody.userId = user.user_id; + } + + helper_utils.sendResponse(ctx, { + status: 200, + body: responseBody, + }); + } catch (error) { + console.log(error); + helper_utils.errorResponse(ctx, 500, "Invalid request"); + return; + } +} + +async function api_login(ctx: Context): Promise { + try { + const body = ctx.request.body; + const result = await body.json(); + const { username, password } = result; + + if (!username || !password) { + helper_utils.errorResponse(ctx, 400, "Missing required fields"); + return "Error"; + } + + const user = await db_utils.getUserByUsername(username); + if (!user) { + helper_utils.errorResponse(ctx, 404, "User not found"); + return "Error"; + } + + const storedSalt = user.password_salt; + const saltedPassword = `${password}${storedSalt}`; + const hash = await helper_utils.hashPassword(saltedPassword); + + // Compare the hashed password with the stored hash + if (user.password !== hash) { + helper_utils.errorResponse(ctx, 401, "Invalid password"); + return "Error"; + } + + // Return success with the user ID if it exists + const responseBody: any = { + success: true, + message: "Login successful", + }; + + // Only add userId if it exists + if (user.user_id !== undefined) { + responseBody.userId = user.user_id; + } + + helper_utils.sendResponse(ctx, { + status: 200, + body: responseBody, + }); + + return "Success"; + } catch (error) { + console.log(error); + helper_utils.errorResponse(ctx, 500, "Invalid request"); + return "Error"; + } +} + +// +++ APP ---------------------------------------------------------- // +app.use( + oakCors({ + origin: "*", + credentials: true, + }), +); +app.use(router.routes()); +app.use(router.allowedMethods()); + +export { app }; +await app.listen({ port: 8000 }); diff --git a/database/create_db.ts b/database/create_db.ts new file mode 100644 index 0000000..c3981dc --- /dev/null +++ b/database/create_db.ts @@ -0,0 +1,107 @@ +/// + +/** + * @author Esad Mustafoski + * @description This file is responsible for creating the database and the tables + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import { + dirname, + fromFileUrl, + join, +} from "https://deno.land/std@0.224.0/path/mod.ts"; + +// +++ VARIABLES ---------------------------------------------------- // +const _dirname: string = dirname(fromFileUrl(import.meta.url)); +const dbPath: string = join(_dirname, "../database/esp-projekt.sqlite"); +const db = new DB(dbPath); + +export function createDatabase(): void { + db.execute(` + CREATE TABLE IF NOT EXISTS accounts ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_group TEXT, + bio TEXT, + displayname TEXT, + username TEXT, + user_email TEXT, + password TEXT, + password_salt TEXT, + firstname TEXT, + surname TEXT, + account_created TEXT, + blocked_users TEXT, + followers TEXT, + following TEXT, + contacts TEXT + ); + + CREATE TABLE IF NOT EXISTS posts ( + posts_uuid INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + created_at TEXT, + post_text TEXT, + post_type TEXT, + likes INTEGER, + comments INTEGER + ); + + CREATE TABLE IF NOT EXISTS comments ( + comment_id INTEGER PRIMARY KEY AUTOINCREMENT, + post_id INTEGER, + author_user_id INTEGER, + date_created_at TEXT, + text TEXT, + likes INTEGER + ); + + CREATE TABLE IF NOT EXISTS messages ( + message_id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id INTEGER, + sender_id INTEGER, + content TEXT, + timestamp TEXT, + FOREIGN KEY (chat_id) REFERENCES chats (chat_id), + FOREIGN KEY (sender_id) REFERENCES accounts (user_id) + ); + + CREATE TABLE IF NOT EXISTS chats ( + chat_id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_name TEXT, + participants TEXT, + created_at TEXT + ) + `); +} + +// Sample data generated using AI, does not work yet and will be adjusted +export function insertSampleData(): void { + db.query( + `INSERT INTO accounts (user_group, bio, displayname, username, user_email, password, password_salt, firstname, surname, account_created, blocked_users, followers, following, contacts) VALUES + ('admin', 'Admin bio', 'Admin User', 'admin', 'admin@example.com', 'pw1', 'salt1', 'Admin', 'User', '2024-01-01', '[]', '[]', '[]', '[]'), + ('user', 'I love coding!', 'John Dev', 'johndev', 'john@example.com', 'pw2', 'salt2', 'John', 'Smith', '2024-01-02', '[]', '[]', '[3,4]', '[1,2]'), + ('user', 'Photography enthusiast', 'Alice', 'alice_photo', 'alice@example.com', 'pw3', 'salt3', 'Alice', 'Johnson', '2024-01-03', '[]', '[5]', '[1]', '[2]') + `, + ); + + db.query( + `INSERT INTO posts (user_id, created_at, post_text, post_type, likes, comments) VALUES + (1, '2024-01-15 10:00:00', 'First post about programming!', 'text', 5, 2), + (1, '2024-01-15 11:30:00', 'Check out this new feature', 'text', 10, 3), + (2, '2024-01-16 09:15:00', 'Just learned about TypeScript', 'text', 8, 1), + (3, '2024-01-16 14:20:00', 'Posted my new photo collection', 'image', 15, 4) + `, + ); + + db.query( + `INSERT INTO comments (post_id, author_user_id, date_created_at, text, likes) VALUES + (1, 2, '2024-01-15 10:05:00', 'Great post!', 3), + (1, 3, '2024-01-15 10:10:00', 'Very informative', 2), + (2, 3, '2024-01-15 11:35:00', 'Nice feature', 4), + (3, 1, '2024-01-16 09:20:00', 'TypeScript is awesome', 5), + (4, 2, '2024-01-16 14:25:00', 'Beautiful photos!', 6) + `, + ); +} diff --git a/database/doc/DATABASE_UTILS.md b/database/doc/DATABASE_UTILS.md new file mode 100644 index 0000000..e69de29 diff --git a/database/helpers/interfaces.ts b/database/helpers/interfaces.ts new file mode 100644 index 0000000..f594231 --- /dev/null +++ b/database/helpers/interfaces.ts @@ -0,0 +1,61 @@ +/// + +/** + * @author Esad Mustafoski + * @description This file is responsible for making Interfaces accessible by every file, deduplicating it. + * @file interfaces.ts + */ + +interface Post { + posts_uuid: number; + user_id: number; + created_at: string; + post_text: string; + post_type: string; + likes: number; + comments: number; +} + +interface Accounts { + user_id: number; + user_group: string; + bio: string; + displayname: string; + username: string; + user_email: string; + password: string; + password_salt: string; + firstname: string; + surname: string; + account_created: string; + blocked_users: string; + followers: string; + following: string; + contacts: string; +} + +interface Comments { + comment_id: number; + post_id: number; + author_user_id: number; + date_created_at: string; + text: string; + likes: number; +} + +interface Chat { + chat_id: number; + chat_name: string; + participants: string; + created_at: string; +} + +interface Message { + message_id: number; + chat_id: number; + sender_id: number; + content: string; + timestamp: string; +} + +export type { Accounts, Chat, Comments, Message, Post }; diff --git a/database/helpers/maphelper.ts b/database/helpers/maphelper.ts new file mode 100644 index 0000000..693bfc1 --- /dev/null +++ b/database/helpers/maphelper.ts @@ -0,0 +1,122 @@ +/// +/** + * @author Esad Mustafoski + * @description This file makes accessing the Database easier by creating a map for each type of data + * @file maphelper.ts + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { Row } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import { + Accounts, + Chat, + Comments, + Message, + Post, +} from "../helpers/interfaces.ts"; + +function mapPostRow(row: Row): Post { + const [ + posts_uuid, + user_id, + created_at, + post_text, + post_type, + likes, + comments, + ] = row; + return { + posts_uuid: Number(posts_uuid), + user_id: Number(user_id), + created_at: String(created_at), + post_text: String(post_text), + post_type: String(post_type), + likes: Number(likes), + comments: Number(comments), + }; +} + +function mapCommentRow(row: Row): Comments { + const [ + comment_id, + post_id, + author_user_id, + date_created_at, + text, + likes, + ] = row; + return { + comment_id: Number(comment_id), + post_id: Number(post_id), + author_user_id: Number(author_user_id), + date_created_at: String(date_created_at), + text: String(text), + likes: Number(likes), + }; +} + +function mapChatRow(row: Row): Chat { + const [chat_id, chat_name, participants, created_at] = row; + return { + chat_id: Number(chat_id), + chat_name: String(chat_name), + participants: String(participants), + created_at: String(created_at), + }; +} + +function mapMessageRow(row: Row): Message { + const [message_id, chat_id, sender_id, content, timestamp] = row; + return { + message_id: Number(message_id), + chat_id: Number(chat_id), + sender_id: Number(sender_id), + content: String(content), + timestamp: String(timestamp), + }; +} + +function mapAccountRow(row: Row): Accounts { + const [ + user_id, + user_group, + bio, + displayname, + username, + user_email, + password, + password_salt, + firstname, + surname, + account_created, + blocked_users, + followers, + following, + contacts, + ] = row; + return { + user_id: Number(user_id), + user_group: String(user_group), + bio: String(bio), + displayname: String(displayname), + username: String(username), + user_email: String(user_email), + password: String(password), + password_salt: String(password_salt), + firstname: String(firstname), + surname: String(surname), + account_created: String(account_created), + blocked_users: String(blocked_users), + followers: String(followers), + following: String(following), + contacts: String(contacts), + }; +} + +export { + mapAccountRow, + mapChatRow, + mapCommentRow, + mapMessageRow, + mapPostRow +}; \ No newline at end of file diff --git a/database/helpers/utils/chat_utils.ts b/database/helpers/utils/chat_utils.ts new file mode 100644 index 0000000..d6ac389 --- /dev/null +++ b/database/helpers/utils/chat_utils.ts @@ -0,0 +1,86 @@ +/// + +/** + * @author Esad Mustafoski + * @description This file is responsible for creating Functions to easily access the Database, Specifically for Chats + * @file chatUtil.ts + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import { Chat, Message } from "../interfaces.ts"; +import { mapChatRow, mapMessageRow, queryDatabase } from "./mod.ts"; + +async function getUserChats(db: DB, userId: string): Promise { + const query = `SELECT * FROM chats WHERE participants LIKE '%${userId}%'`; + return await queryDatabase(db, query, [], mapChatRow); +} + +async function getChatById(db: DB, chatId: string): Promise { + const query = `SELECT * FROM chats WHERE chat_id = ?`; + const chats = await queryDatabase(db, query, [chatId], mapChatRow); + return chats.length > 0 ? chats[0] : null; +} + +async function getChatMessages(db: DB, chatId: string): Promise { + const query = + `SELECT * FROM messages WHERE chat_id = ? ORDER BY timestamp ASC`; + return await queryDatabase(db, query, [chatId], mapMessageRow); +} + +async function createChat( + db: DB, + participants: string[], + chatName: string, +): Promise { + const participantsJson = JSON.stringify(participants); + const timestamp = `${Math.floor(Date.now() / 1000)}-${ + new Date().toLocaleDateString("en-GB").split("/").join("-") + }`; + + const query = ` + INSERT INTO chats (chat_name, participants, created_at) + VALUES (?, ?, ?) + `; + + db.query(query, [chatName, participantsJson, timestamp]); + return db.lastInsertRowId.toString(); +} + +async function addMessageToChat( + db: DB, + chatId: string, + senderId: string, + content: string, +): Promise { + const timestamp = `${Math.floor(Date.now() / 1000)}-${ + new Date().toLocaleDateString("en-GB").split("/").join("-") + }`; + + const query = ` + INSERT INTO messages (chat_id, sender_id, content, timestamp) + VALUES (?, ?, ?, ?) + `; + + db.query(query, [chatId, senderId, content, timestamp]); + return db.lastInsertRowId.toString(); +} + +async function deleteChat(db: DB, chatId: string): Promise { + // First delete all messages in the chat + const deleteMessagesQuery = `DELETE FROM messages WHERE chat_id = ?`; + db.query(deleteMessagesQuery, [chatId]); + + // Then delete the chat itself + const deleteChatQuery = `DELETE FROM chats WHERE chat_id = ?`; + db.query(deleteChatQuery, [chatId]); +} + +export { + getUserChats, + getChatById, + getChatMessages, + createChat, + addMessageToChat, + deleteChat +}; \ No newline at end of file diff --git a/database/helpers/utils/comment_utils.ts b/database/helpers/utils/comment_utils.ts new file mode 100644 index 0000000..8abba7a --- /dev/null +++ b/database/helpers/utils/comment_utils.ts @@ -0,0 +1,81 @@ +/// + +/** + * @author Esad Mustafoski + * @description This file is responsible for creating Functions to easily access the Database, Specifically for Comments + * @file commentUtil.ts + */ + +import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import { Comments } from "../interfaces.ts"; +import { mapCommentRow, queryDatabase } from "./mod.ts"; + +async function getCommentsFromDB( + db: DB, + post_id?: number, +): Promise { + const query = post_id + ? `SELECT * FROM comments WHERE post_id = ?` + : `SELECT * FROM comments`; + const params = post_id ? [post_id] : []; + return await queryDatabase(db, query, params, mapCommentRow); +} + +function createComment( + db: DB, + postId: string, + userId: string, + createdAt: string, + text: string, +): string { + const query = ` + INSERT INTO comments (post_id, author_user_id, date_created_at, text, likes) + VALUES (?, ?, ?, ?, 0) + `; + db.query(query, [postId, userId, createdAt, text]); + + const updatePostQuery = + `UPDATE posts SET comments = comments + 1 WHERE posts_uuid = ?`; + db.query(updatePostQuery, [postId]); + + return db.lastInsertRowId.toString(); +} + +async function updateComment( + db: DB, + commentId: string, + text: string, +): Promise { + const query = `UPDATE comments SET text = ? WHERE comment_id = ?`; + db.query(query, [text, commentId]); +} + +async function deleteComment(db: DB, commentId: string): Promise { + const getPostIdQuery = `SELECT post_id FROM comments WHERE comment_id = ?`; + const result = db.query(getPostIdQuery, [commentId]); + const postId: any = result[0][0]; + + const deleteCommentQuery = `DELETE FROM comments WHERE comment_id = ?`; + db.query(deleteCommentQuery, [commentId]); + + const updatePostQuery = + `UPDATE posts SET comments = comments - 1 WHERE posts_uuid = ?`; + db.query(updatePostQuery, [postId]); +} + +async function likeComment( + db: DB, + commentId: string, + userId: string, +): Promise { + const query = `UPDATE comments SET likes = likes + 1 WHERE comment_id = ?`; + db.query(query, [commentId]); +} + +export { + createComment, + deleteComment, + getCommentsFromDB, + likeComment, + updateComment, +}; diff --git a/database/helpers/utils/core_utils.ts b/database/helpers/utils/core_utils.ts new file mode 100644 index 0000000..84a2bb5 --- /dev/null +++ b/database/helpers/utils/core_utils.ts @@ -0,0 +1,46 @@ +/// +/** + * @author Esad Mustafoski + * @description Core Utility for querying the Database and inserting Sample Data + * @file core_utils.ts + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { DB, Row } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import * as db_create from "../../create_db.ts"; + +// +++ Helper ------------------------------------------------------ // +// "T" is a generic type, it can be anything and makes the function "flexible"(?) +async function queryDatabase( + db: DB, + query: string, + params: any[], + mapRow: (row: Row) => T, +): Promise { + const results: T[] = []; + try { + const rows = await db.query(query, params); + for (const row of rows) { + results.push(mapRow(row)); + } + } catch (error) { + console.error("Database query error:", error); + } + return results; +} + +// +++ FUNCTIONS --------------------------------------------------- // + +/** + * See: + * @file ./create_db.ts + */ +function insertSamples(): void { + db_create.insertSampleData(); +} + +function createDatabaseIfNotExist(): void { + db_create.createDatabase(); +} + +export { queryDatabase, insertSamples, createDatabaseIfNotExist }; \ No newline at end of file diff --git a/database/helpers/utils/mod.ts b/database/helpers/utils/mod.ts new file mode 100644 index 0000000..f66c7ed --- /dev/null +++ b/database/helpers/utils/mod.ts @@ -0,0 +1,13 @@ +/// +/** + * @author Esad Mustafoski + * @description A mod file is used to export all the functions in the folder, making them easier to access. + * @file mod.ts + */ + +export * from "./chat_utils.ts"; +export * from "./comment_utils.ts"; +export * from "./post_utils.ts"; +export * from "./user_utils.ts"; +export * from "./core_utils.ts" +export * from "../maphelper.ts"; \ No newline at end of file diff --git a/database/helpers/utils/post_utils.ts b/database/helpers/utils/post_utils.ts new file mode 100644 index 0000000..dadb760 --- /dev/null +++ b/database/helpers/utils/post_utils.ts @@ -0,0 +1,128 @@ +/// + +/** + * @author Esad Mustafoski + * @description This file is responsible for creating Functions to easily access the Database, Specifically for Posts + * @file postUtil.ts + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import { Post } from "../interfaces.ts"; +import { mapPostRow, queryDatabase } from "./mod.ts"; + +async function getPostsFromDB(db: DB, user_uuid?: string): Promise { + const query = user_uuid + ? `SELECT * FROM posts WHERE user_id = ?` + : `SELECT * FROM posts`; + const params = user_uuid ? [user_uuid] : []; + return await queryDatabase(db, query, params, mapPostRow); +} + +async function getPostById(db: DB, postId: string): Promise { + const query = `SELECT * FROM posts WHERE posts_uuid = ?`; + const posts = await queryDatabase(db, query, [postId], mapPostRow); + return posts.length > 0 ? posts[0] : null; +} + +async function createPost( + db: DB, + userId: string, + createdAt: string, + postText: string, + postType: string, +): Promise { + const query = ` + INSERT INTO posts (user_id, created_at, post_text, post_type, likes, comments) + VALUES (?, ?, ?, ?, 0, 0) + `; + + db.query(query, [userId, createdAt, postText, postType]); + return db.lastInsertRowId.toString(); +} + +async function updatePost( + db: DB, + postId: string, + postText?: string, + postType?: string, +): Promise { + let query = `UPDATE posts SET `; + const params: any[] = []; + + if (postText) { + query += `post_text = ?`; + params.push(postText); + + if (postType) { + query += `, post_type = ?`; + params.push(postType); + } + } else if (postType) { + query += `post_type = ?`; + params.push(postType); + } + + query += ` WHERE posts_uuid = ?`; + params.push(postId); + + db.query(query, params); +} + +// This function deletes the comments on the post first, then +// deletes the post to avoid errors +async function deletePost(db: DB, postId: string): Promise { + const deleteCommentsQuery = `DELETE FROM comments WHERE post_id = ?`; + db.query(deleteCommentsQuery, [postId]); + + const deletePostQuery = `DELETE FROM posts WHERE posts_uuid = ?`; + db.query(deletePostQuery, [postId]); +} + +// This is simplified and doesn't work as it would in a real application +// or website like twitter, this only exists as a test +async function likePost(db: DB, postId: string, userId: string): Promise { + const query = `UPDATE posts SET likes = likes + 1 WHERE posts_uuid = ?`; + db.query(query, [postId]); +} + +/** + * @param posts_to_filter The Posts in an array to filter + * @param post_types The types of Posts to filter for + * @returns Array of Posts + */ +// Filter functions merged to one +function filterPosts(posts_to_filter: Post[], post_types: string[]): Post[] { + if (post_types.length === 0) { + return posts_to_filter; + } + + // Set is a type that we can specify the values to + // example: const set = new Set(); + // set.add(1); Will work + // set.add("2"); Will not work, it will error because + // it is a string. + const includedPostIds = new Set(); + const result: Post[] = []; + + for (const type of post_types) { + for (const post of posts_to_filter) { + if (!includedPostIds.has(post.posts_uuid) && post.post_type === type) { + result.push(post); + includedPostIds.add(post.posts_uuid); + } + } + } + + return result; +} + +export { + getPostsFromDB, + getPostById, + createPost, + updatePost, + deletePost, + likePost, + filterPosts, +}; diff --git a/database/helpers/utils/user_utils.ts b/database/helpers/utils/user_utils.ts new file mode 100644 index 0000000..d2f2c93 --- /dev/null +++ b/database/helpers/utils/user_utils.ts @@ -0,0 +1,103 @@ +/// + +/** + * @author Esad Mustafoski + * @description This file is responsible for creating Functions to easily access the Database, Specifically for Users + * @file userUtil.ts + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { DB, Row } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import { mapAccountRow, queryDatabase } from "./mod.ts"; +import { Accounts } from "../interfaces.ts"; + +/** + * @param user The username of the User to add + * @param password The hashed password of the User to add + * @param salt The salt used for the password + * @returns "noUser" if user exists, "newUser" if registration successful + */ +function registerUser( + db: DB, + user: string, + password: string, + salt: string, + userGroup: string, + displayname: string, + user_email: string, + firstname: string, + surname: string, + account_created: string, +): any { + const query_user_exists = + `SELECT * FROM accounts WHERE displayname = '${user}'`; + if (!query_user_exists) { + return "noUser"; + } + + const query_add_user = ` + INSERT INTO accounts ( + username, + password, + password_salt, + user_group, + displayname, + user_email, + firstname, + surname, + account_created, + bio, + blocked_users, + followers, + following, + contacts + ) VALUES ( + '${user}', + '${password}', + '${salt}', + '${userGroup}', + '${displayname}', + '${user_email}', + '${firstname}', + '${surname}', + '${account_created}', + '', + '[]', + '[]', + '[]', + '[]' + )`; + db.query(query_add_user); + const userId = db.query( + `SELECT user_id FROM accounts WHERE displayname = '${user}'`, + ); + console.log(`New user: ${user}`); + + return userId; +} + +/** + * @returns Array of all Users in the Database + */ +async function getAllUsersFromDB(db: DB): Promise { + const query = `SELECT * FROM accounts`; + return await queryDatabase(db, query, [], mapAccountRow); +} + +/** + * @param username + * @returns Returns the Accounts for the User with the given username + */ +async function getUserByUsername(db: DB, username: string): Promise { + const query = `SELECT * FROM accounts WHERE username = '${username}'`; + const params: string[] = []; + const result = await queryDatabase( + db, + query, + params, + mapAccountRow, + ); + return result[0]; +} + +export { getAllUsersFromDB, getUserByUsername, registerUser }; diff --git a/database/utils.ts b/database/utils.ts new file mode 100644 index 0000000..f0465c4 --- /dev/null +++ b/database/utils.ts @@ -0,0 +1,203 @@ +/// + +/** + * @author Esad Mustafoski + * @description This file is responsible for creating Functions to easily access the Database, Intended for use in the API + */ + +// +++ IMPORTS ------------------------------------------------------ // +import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; +import { + dirname, + fromFileUrl, + join, +} from "https://deno.land/std@0.224.0/path/mod.ts"; +import * as db_create from "./create_db.ts"; + +// Import all internal utilities with renamed imports to avoid naming conflicts +import { + addMessageToChat as addMessageToChatInternal, + createChat as createChatInternal, + // getCommentsForComments as getCommentsForCommentsInternal, + // Accidentally deleted function... + createComment as createCommentInternal, + createPost as createPostInternal, + deleteChat as deleteChatInternal, + deleteComment as deleteCommentInternal, + deletePost as deletePostInternal, + filterPosts, + // --- Account Functions --- // + getAllUsersFromDB as getAllUsersFromDBInternal, + getChatById as getChatByIdInternal, + getChatMessages as getChatMessagesInternal, + // --- Comment Functions --- // + getCommentsFromDB as getCommentsFromDBInternal, + getPostById as getPostByIdInternal, + // --- Post Functions --- // + getPostsFromDB as getPostsFromDBInternal, + getUserByUsername as getUserByUsernameInternal, + // --- Chat Functions --- // + getUserChats as getUserChatsInternal, + likeComment as likeCommentInternal, + likePost as likePostInternal, + // --- Mapper Functions --- // + queryDatabase as queryDatabaseInternal, + // getAllUserInfoByID as getAllUserInfoByIDInternal, + // Accidentally deleted function... + registerUser as registerUserInternal, + updateComment as updateCommentInternal, + updatePost as updatePostInternal, +} from "./helpers/utils/mod.ts"; + +// +++ VARIABLES ---------------------------------------------------- // +// _dirname Is never getting used again, It's only needed because the DB Import +// from SQLite doesn't like relative paths, so I use this as +// A Workaround +const _dirname: string = dirname(fromFileUrl(import.meta.url)); +const dbPath: string = join(_dirname, "../database/esp-projekt.sqlite"); +const db = new DB(dbPath); + +// +++ INTERFACES --------------------------------------------------- // +// Only re-export interfaces that are needed by external code +export type { + Accounts, + Chat, + Comments, + Message, + Post, +} from "./helpers/interfaces.ts"; +// +++ HELPER FUNCTIONS --------------------------------------------- // +export function queryDatabase( + query: string, + params: any[], + mapRow: (row: any) => T, +): Promise { + return queryDatabaseInternal(db, query, params, mapRow); +} + +// +++ DATABASE INITIALIZATION -------------------------------------- // +export async function ensureDatabaseExists(): Promise { + try { + // Check if the database directory exists, create it if not + const dbDir = dirname(dbPath); + try { + await Deno.stat(dbDir); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + // Create the database directory + await Deno.mkdir(dbDir, { recursive: true }); + console.log(`Created database directory: ${dbDir}`); + } else { + throw error; + } + } + + // Check if the database file exists + try { + await Deno.stat(dbPath); + console.log("Database file already exists"); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + createDatabaseIfNotExist(); + insertSamples(); + // Nothing, file will be created below + } else { + throw error; + } + } + } catch (error) { + console.error("Error ensuring database exists:", error); + throw error; + } +} + +export function createDatabaseIfNotExist(): void { + db_create.createDatabase(); +} + +export function insertSamples(): void { + db_create.insertSampleData(); +} + +// +++ ACCOUNT FUNCTIONS -------------------------------------------- // +export const getAllUsersFromDB = () => getAllUsersFromDBInternal(db); +export const getUserByUsername = (username: string) => + getUserByUsernameInternal(db, username); +// export const getAllUserInfoByID = (user_id: string) => getAllUserInfoByIDInternal(db, user_id); +export const registerUser = ( + user: string, + password: string, + salt: string, + userGroup: string, + displayname: string, + user_email: string, + firstname: string, + surname: string, + account_created: string, +) => + registerUserInternal( + db, + user, + password, + salt, + userGroup, + displayname, + user_email, + firstname, + surname, + account_created, + ); + +// +++ POST FUNCTIONS ----------------------------------------------- // +export const getPostsFromDB = (user_uuid?: string) => + getPostsFromDBInternal(db, user_uuid); +export const getPostById = (postId: string) => getPostByIdInternal(db, postId); +export const createPost = ( + userId: string, + createdAt: string, + postText: string, + postType: string, +) => createPostInternal(db, userId, createdAt, postText, postType); +export const updatePost = ( + postId: string, + postText?: string, + postType?: string, +) => updatePostInternal(db, postId, postText, postType); +export const deletePost = (postId: string) => deletePostInternal(db, postId); +export const likePost = (postId: string, userId: string) => + likePostInternal(db, postId, userId); + +// +++ COMMENT FUNCTIONS -------------------------------------------- // +export const getCommentsFromDB = (post_id?: number) => + getCommentsFromDBInternal(db, post_id); +// export const getCommentsForComments = (comment_id: number) => getCommentsForCommentsInternal(db, comment_id); +export const createComment = ( + postId: string, + userId: string, + createdAt: string, + text: string, +) => createCommentInternal(db, postId, userId, createdAt, text); +export const updateComment = (commentId: string, text: string) => + updateCommentInternal(db, commentId, text); +export const deleteComment = (commentId: string) => + deleteCommentInternal(db, commentId); +export const likeComment = (commentId: string, userId: string) => + likeCommentInternal(db, commentId, userId); + +// +++ CHAT FUNCTIONS ----------------------------------------------- // +export const getUserChats = (userId: string) => + getUserChatsInternal(db, userId); +export const getChatById = (chatId: string) => getChatByIdInternal(db, chatId); +export const getChatMessages = (chatId: string) => + getChatMessagesInternal(db, chatId); +export const createChat = (participants: string[], chatName: string) => + createChatInternal(db, participants, chatName); +export const addMessageToChat = ( + chatId: string, + senderId: string, + content: string, +) => addMessageToChatInternal(db, chatId, senderId, content); +export const deleteChat = (chatId: string) => deleteChatInternal(db, chatId); + +// +++ UTILITY FUNCTIONS -------------------------------------------- // +export { filterPosts };