diff --git a/.gitignore b/.gitignore index ce13133..19484b1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ dist-ssr .vite *.local *.sqlite +*.idea +.env # Editor directories and files .vscode/* diff --git a/api/helpers.ts b/api/helpers.ts new file mode 100644 index 0000000..477c7e4 --- /dev/null +++ b/api/helpers.ts @@ -0,0 +1,44 @@ +/// + +/** + * @author Esad Mustafoski + * @file api/helpers.ts + * @description Helper functions for the API + * + */ +import { Context } from "https://deno.land/x/oak/mod.ts"; +import { hash } from "node:crypto"; + +export type ApiResponse = { + status: number; + body: unknown; +}; + +// --- Helper 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 Hashing Function for Passwords/etc + * @param password The password to hash + * Works only up to 5 Megabytes + */ +const hashPassword = async (password: string): Promise => { + return hash("sha256", password); +} \ No newline at end of file diff --git a/api/main.ts b/api/main.ts index 0c47690..5ef4864 100644 --- a/api/main.ts +++ b/api/main.ts @@ -5,35 +5,156 @@ */ // +++ IMPORTS ------------------------------------------------------ // -import { Application, Router } from "https://deno.land/x/oak/mod.ts"; +import { Application, Router, Context, Next } from "https://deno.land/x/oak/mod.ts"; import { oakCors } from "https://deno.land/x/cors/mod.ts"; import * as db_utils from "../database/utils.ts"; +import * as helper_utils from "./helpers.ts"; -// +++ VARIABLES ---------------------------------------------------- // +// +++ VARIABLES / TYPES --------------------------------------------- // const router = new Router(); const app = new Application(); +// For the future +type ApiResponse = { + status: number; + body: unknown; + } + // +++ ROUTER ------------------------------------------------------- // // Creates the routes for the API server. // Example: localhost:8000/api will show "testAPIPoint" -// in the HTML -router - .get("/", (ctx):void => { - ctx.response.body = "ESP API Site"; - }) - .get("/api", (ctx):void => { - ctx.response.body = "testAPIPoint"; - }) - .get("/api/users", async (ctx):Promise => { - const getUsers = await db_utils.getAllUsersFromDB(); - ctx.response.body = getUsers; //getAllUsers(); - }) - .get("/api/posts", async (ctx):Promise => { - const getPosts = await db_utils.getPostsFromDB(); - const countedPosts:number = await db_utils.countPosts(); - ctx.response.body = { getPosts, countedPosts }; - }); +// in the HTML page. +// Docs Routes +router + .get("/", (ctx) => { ctx.response.body = "For endpoints, use /api/{name}"; }) + .get("/api", (ctx) => { ctx.response.body = "For API Documentation, visit /docs"; }) + +// Account routes +router + .post("/api/account/login", () => {}) // TODO + .post("/api/account/register", () => {}) // TODO + .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); + +// Post routes +router + .get("/api/posts", api_posts_getAll); + +// +++ FUNCTIONS ----------------------------------------------------- // + + + + +/** + * @description Stands between the client and the API + * It checks if the client is authorized to access the API with a token/Multiple tokens + * Currently not implemented + * Middleware + */ +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; + } + + 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, not tried or attempted yet. + 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 is missing, return 401 + * 5. if token is expired, return 401 + * 6. if token is blacklisted, return 401 + * 7. if token is not in the database, return 401 + * 8. if token is not associated with a user, return 401 + * 9. if token is not associated with the correct user, return 401 + */ +} + + +async function api_getAllUsers(ctx: any): Promise { + const getUsers = await db_utils.getAllUsersFromDB(); + ctx.response.body = getUsers; +} + +// Users +async function api_user_getInfo(ctx: any): Promise { + const id = ctx.params.id; + + if (!id) { + ctx.response.status = 400; // Bad Request status + ctx.response.body = { error: "User ID required" }; + return; + } + + try { + const user = await db_utils.getAllUserInfoByID(id); + + if (!user) { + ctx.response.status = 404; // Not Found status/Doesn't exist + ctx.response.body = { error: "User not found" }; + return; + } + + ctx.response.body = user; + } catch (error) { + ctx.response.status = 500; // Internal Server Error status + ctx.response.body = { error: "Error" }; + } +} + + +// Posts +async function api_posts_getAll(ctx: any): Promise { + const posts = await db_utils.getPostsFromDB(); + ctx.response.body = posts; +} + +// Comments + +// Filtering + +// +++ APP ---------------------------------------------------------- // app.use(oakCors()); app.use(router.routes()); app.use(router.allowedMethods()); diff --git a/database/create_db.ts b/database/create_db.ts index 964c378..932c7f3 100644 --- a/database/create_db.ts +++ b/database/create_db.ts @@ -1,10 +1,10 @@ +/// + /** * @author Esad Mustafoski * @description This file is responsible for creating the database and the tables */ -/// - // +++ IMPORTS ------------------------------------------------------ // import { DB } from "https://deno.land/x/sqlite/mod.ts"; import { dirname, fromFileUrl, join } from "https://deno.land/std/path/mod.ts"; @@ -50,9 +50,9 @@ export function createDatabase(): void { likes INTEGER ) `); -} +}; -// Sample data generated using online generators +// Sample data generated using AI, does not work yet and will be adjusted export function insertSampleData(): void { db.query(`INSERT INTO accounts (user_group, user_bio, user_displayname, user_username, user_email, password, firstname, surname, account_created, followers, following, contacts) VALUES ('admin', 'Admin bio', 'Admin User', 'admin', 'admin@example.com', 'hashedpass1', 'Admin', 'User', '2024-01-01', '[]', '[]', '[]', '[]'), diff --git a/database/utils.ts b/database/utils.ts index 337b5c6..38d1c0d 100644 --- a/database/utils.ts +++ b/database/utils.ts @@ -56,8 +56,93 @@ interface Comments { likes: number; } -// +++ FUNCTIONS---------------------------------------------------- // +// +++ Helper ------------------------------------------------------ // +function mapPostRow(row: Row): Post { + const [posts_uuid, user_id, created_at, post_text, likes, comments] = row; + return { + posts_uuid: Number(posts_uuid), + user_id: Number(user_id), + created_at: String(created_at), + post_text: String(post_text), + likes: Number(likes), + comments: Number(comments), + }; +} +function mapAccountRow(row: Row): Accounts { + const [ + user_id, + user_group, + bio, + displayname, + username, + user_email, + password, + 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), + 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), + }; +} + +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), + }; +} + +// "T" is a generic type, it can be anything and makes the function "flexible"(?) +async function queryDatabase(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(); } @@ -73,110 +158,19 @@ function createDatabaseIfNotExist(): void { * @returns Array of all Posts in the Database */ async function getPostsFromDB(user_uuid?: string): Promise { - const data_result: Array = []; - let rows: Row[]; - - try { - try { - if (user_uuid === undefined) { - rows = await db.query(`SELECT * FROM posts`); - } else { - rows = await db.query( - `SELECT * FROM posts WHERE user_id = ${user_uuid}`, - ); - } - } catch (error) { - console.error("Error fetching posts", error, "User UUID:", user_uuid); - return []; - } - - for (const row of rows) { - const [ - posts_uuid, - user_id, - created_at, - post_text, - likes, - comments, - ] = row; - - data_result.push({ - posts_uuid: Number(posts_uuid), - user_id: Number(user_id), - created_at: String(created_at), - post_text: String(post_text), - likes: Number(likes), - comments: Number(comments), - }); - } - } catch (error) { - console.error("Error fetching posts", error); - } - return data_result; -} + const query = user_uuid + ? `SELECT * FROM posts WHERE user_id = ?` + : `SELECT * FROM posts`; + const params = user_uuid ? [user_uuid] : []; + return await queryDatabase(query, params, mapPostRow); + } /** * @returns Array of all Users in the Database */ async function getAllUsersFromDB(): Promise { - const accounts_list: Array = []; - try { - const rows = await db.query("SELECT * FROM accounts"); - - for (const row of rows) { - const [ - user_id, - user_group, - bio, - displayname, - username, - user_email, - password, - firstname, - surname, - account_created, - blocked_users, - followers, - following, - contacts, - ] = row; - accounts_list.push({ - 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), - 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), - }); - } - } catch (error) { - console.error("Error fetching users", error); - } - return accounts_list; -} - -// Test Function, not useful -// Promise needed because of "await" -// It indicates that the function resolves something -async function countPosts(): Promise { - let count = 0; - try { - for (const [c] of await db.query("SELECT COUNT(*) FROM posts")) { - count = c as number; - } - } catch (error) { - console.error("Error counting posts:", error); - } - console.log("Total posts:", count); - return count; + const query = `SELECT * FROM accounts`; + return await queryDatabase(query, [], mapAccountRow); } /** @@ -186,43 +180,11 @@ async function countPosts(): Promise { * @returns Array of Comments for the Post, or an empty Array if there are no Comments */ async function getCommentsFromDB(post_id?: number): Promise { - const data_result: Array = []; - let rows: Row[] = []; - - try { - if (post_id === undefined) { - rows = await db.query(`SELECT * FROM comments`); - } else { - rows = await db.query( - `SELECT * FROM comments WHERE post_id = ${post_id}`, - ); - } - - for (const row of rows) { - const [ - comment_id, - post_id, - author_user_id, - date_created_at, - text, - likes, - ] = row; - - data_result.push({ - 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), - }); - } - } catch (error) { - console.error("Error fetching comments", error, "Post ID:", post_id); - return []; - } - - return data_result; + const query = post_id + ? `SELECT * FROM comments WHERE post_id = ?` + : `SELECT * FROM comments`; + const params = post_id ? [post_id] : []; + return await queryDatabase(query, params, mapCommentRow); } /** @@ -238,92 +200,15 @@ function getCommentsForComments(comment_id: number) { * Included: Comments, Posts... Everything including the specific user_uuid in the database * Might be required for Administrating the User */ -async function getAllUserInfoByID(user_id: number) { - const data_result_account: Array = []; - const data_result_post: Array = []; - const data_result_comments: Array = []; - - try { - const [rowsAccount, rowsPost, rowsComments] = await Promise.all([ - db.query("SELECT * FROM accounts WHERE uuid = ?", [user_id]), - db.query("SELECT * FROM posts WHERE uuid = ?", [user_id]), - db.query("SELECT * FROM comments WHERE uuid = ?", [user_id]), - ]); - - for (const row in rowsAccount) { - const [user_id,user_group,bio,displayname,username,user_email,password, - firstname,surname,account_created,blocked_users,followers,following, - contacts, - ] = row; - data_result_account.push({ - 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), - 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), - }); - } - for (const row in rowsPost) { - const [ - posts_uuid, - user_id, - created_at, - post_text, - likes, - comments, - ] = row; - data_result_post.push({ - posts_uuid: Number(posts_uuid), - user_id: Number(user_id), - created_at: String(created_at), - post_text: String(post_text), - likes: Number(likes), - comments: Number(comments), - }); - } - - for (const row in rowsComments) { - const [ - comment_id, - post_id, - author_user_id, - date_created_at, - text, - likes, - ] = row; - data_result_comments.push({ - 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), - }); - } - - } catch (error) { - console.error(`Failed to get User Info ${error}`); - return []; - } - - const result = { data_result_account, data_result_comments, data_result_post }; - return result; +async function getAllUserInfoByID(user_id: string) { + // Will be rewritten to use the queryDatabase function } /** * @param user_id The ID of the User to get the Posts for * @returns Array of Posts from the User, or an empty Array if the User doesn't exist or has no Posts */ -// Filter Functions +// Filter Functions, Not yet implemented function filterForImagePosts(posts_to_filter: Array) { return []; } @@ -339,14 +224,10 @@ function filterForTextPosts(posts_to_filter: Array) { // Exporting all functions as this is a module export { createDatabaseIfNotExist, - insertSamples, - countPosts, - filterForImagePosts, - filterForTextPosts, - filterForVideoPosts, getAllUserInfoByID, getAllUsersFromDB, getCommentsForComments, getCommentsFromDB, getPostsFromDB, + insertSamples, }; diff --git a/deno.lock b/deno.lock index 84070a3..28a473c 100644 --- a/deno.lock +++ b/deno.lock @@ -1121,6 +1121,11 @@ "https://deno.land/x/superoak/mod.ts": "https://deno.land/x/superoak@4.8.1/mod.ts" }, "remote": { + "https://deno.land/std@0.115.0/_wasm_crypto/crypto.js": "1c565287b35f6eb1aa58499d0f4fbac99a9c30eb9a735c512d193a6493499e84", + "https://deno.land/std@0.115.0/_wasm_crypto/crypto.wasm.js": "e93d38b215c05c552669e9565654599b13e7898ebd3f10ac91e39413efda4b84", + "https://deno.land/std@0.115.0/_wasm_crypto/mod.ts": "9afe300945fe7e5bcec231b52b0016d8442d026b823f619bb5939b2cf66ff21b", + "https://deno.land/std@0.115.0/crypto/mod.ts": "8e1ec0ff94a4f08e3c4f72de5b88781566481602614e62291aa7ae7444ba11f0", + "https://deno.land/std@0.115.0/encoding/base64.ts": "0b58bd6477214838bf711eef43eac21e47ba9e5c81b2ce185fe25d9ecab3ebb3", "https://deno.land/std@0.115.1/async/deadline.ts": "1d6ac7aeaee22f75eb86e4e105d6161118aad7b41ae2dd14f4cfd3bf97472b93", "https://deno.land/std@0.115.1/async/debounce.ts": "b2f693e4baa16b62793fd618de6c003b63228db50ecfe3bd51fc5f6dc0bc264b", "https://deno.land/std@0.115.1/async/deferred.ts": "ab60d46ba561abb3b13c0c8085d05797a384b9f182935f051dc67136817acdee",