diff --git a/config/environment.ts b/config/environment.ts index 7adf22f..9370a4b 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -1,3 +1,5 @@ +import { resolve } from "path"; + export const environment: Environment = { port: parseInt(process.env.PORT || "8080", 10), host: process.env.HOST || "0.0.0.0", @@ -25,3 +27,11 @@ export const jwt: { secret: process.env.JWT_SECRET || "", expiresIn: process.env.JWT_EXPIRES || "1d", }; + +export const dataType: { type: string; path: string | undefined } = { + type: process.env.DATASOURCE_TYPE || "local", + path: + process.env.DATASOURCE_TYPE === "local" + ? resolve(process.env.DATASOURCE_LOCAL_DIRECTORY || "./uploads") + : undefined, +}; diff --git a/config/sql/settings.ts b/config/sql/settings.ts index 11c4748..1ddf8b1 100644 --- a/config/sql/settings.ts +++ b/config/sql/settings.ts @@ -1,7 +1,7 @@ import { logger } from "@helpers/logger"; import { type ReservedSQL, sql } from "bun"; -const defaultSettings: { key: string; value: string }[] = [ +const defaultSettings: Setting[] = [ { key: "default_role", value: "user" }, { key: "default_timezone", value: "UTC" }, { key: "server_timezone", value: "UTC" }, @@ -114,10 +114,10 @@ export async function setSetting( try { await reservation` - INSERT INTO settings ("key", "value") - VALUES (${key}, ${value}) - ON CONFLICT ("key") - DO UPDATE SET "value" = ${value};`; + INSERT INTO settings ("key", "value", updated_at) + VALUES (${key}, ${value}, NOW()) + ON CONFLICT ("key") + DO UPDATE SET "value" = ${value}, "updated_at" = NOW();`; } catch (error) { logger.error(["Could not set the setting:", error as Error]); throw error; diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts index e1fccb8..e95ac5d 100644 --- a/src/helpers/auth.ts +++ b/src/helpers/auth.ts @@ -23,8 +23,8 @@ export async function authByToken( if (!authorizationToken || !isUUID(authorizationToken)) return null; try { - const result: UserSession[] = - await reservation`SELECT id, username, email, roles, avatar, timezone, authorization_token FROM users WHERE authorization_token = ${authorizationToken};`; + const result: User[] = + await reservation`SELECT * FROM users WHERE authorization_token = ${authorizationToken};`; if (result.length === 0) return null; @@ -33,7 +33,7 @@ export async function authByToken( username: result[0].username, email: result[0].email, email_verified: result[0].email_verified, - roles: result[0].roles, + roles: result[0].roles[0].split(","), avatar: result[0].avatar, timezone: result[0].timezone, authorization_token: result[0].authorization_token, diff --git a/src/index.ts b/src/index.ts index 7cddd5d..3170c02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ +import { dataType } from "@config/environment"; import { logger } from "@helpers/logger"; -import { type ReservedSQL, sql } from "bun"; +import { type ReservedSQL, s3, sql } from "bun"; +import { existsSync, mkdirSync } from "fs"; import { readdir } from "fs/promises"; import { resolve } from "path"; @@ -40,6 +42,41 @@ async function main(): Promise { process.exit(1); } + if (dataType.type === "local" && dataType.path) { + if (!existsSync(dataType.path)) { + try { + mkdirSync(dataType.path); + } catch (error) { + logger.error([ + "Could not create datasource local directory", + error as Error, + ]); + process.exit(1); + } + } + + logger.info([ + "Using local datasource directory", + `${dataType.path}`, + ]); + } else { + try { + await s3.write("test", "test"); + await s3.delete("test"); + + logger.info([ + "Connected to S3 with bucket", + `${process.env.S3_BUCKET}`, + ]); + } catch (error) { + logger.error([ + "Could not establish a connection to S3 bucket:", + error as Error, + ]); + process.exit(1); + } + } + await redis.initialize(); serverHandler.initialize(); await initializeDatabase(); diff --git a/src/routes/api/auth/login.ts b/src/routes/api/auth/login.ts index e191151..8cfa4fd 100644 --- a/src/routes/api/auth/login.ts +++ b/src/routes/api/auth/login.ts @@ -141,7 +141,7 @@ async function handler( username: user.username, email: user.email, email_verified: user.email_verified, - roles: user.roles, + roles: user.roles[0].split(","), avatar: user.avatar, timezone: user.timezone, authorization_token: user.authorization_token, diff --git a/src/routes/api/auth/register.ts b/src/routes/api/auth/register.ts index 1f30fe5..20277a8 100644 --- a/src/routes/api/auth/register.ts +++ b/src/routes/api/auth/register.ts @@ -202,7 +202,7 @@ async function handler( username: user.username, email: user.email, email_verified: user.email_verified, - roles: user.roles, + roles: user.roles[0].split(","), avatar: user.avatar, timezone: user.timezone, authorization_token: user.authorization_token, diff --git a/src/routes/api/invite/delete[invite].ts b/src/routes/api/invite/delete[invite].ts index 96cf1b9..34b670e 100644 --- a/src/routes/api/invite/delete[invite].ts +++ b/src/routes/api/invite/delete[invite].ts @@ -1,3 +1,4 @@ +import { isValidInvite } from "@config/sql/users"; import { type ReservedSQL, sql } from "bun"; import { logger } from "@/helpers/logger"; @@ -34,6 +35,19 @@ async function handler(request: ExtendedRequest): Promise { ); } + const { valid, error } = isValidInvite(invite); + + if (!valid && error) { + return Response.json( + { + success: false, + code: 400, + error: error, + }, + { status: 400 }, + ); + } + const reservation: ReservedSQL = await sql.reserve(); let inviteData: Invite | null = null; diff --git a/src/routes/api/settings/set.ts b/src/routes/api/settings/set.ts new file mode 100644 index 0000000..2b0a742 --- /dev/null +++ b/src/routes/api/settings/set.ts @@ -0,0 +1,92 @@ +import { setSetting } from "@config/sql/settings"; + +import { logger } from "@/helpers/logger"; + +const routeDef: RouteDef = { + method: "POST", + accepts: "application/json", + returns: "application/json", + needsBody: "json", +}; + +async function handler( + request: ExtendedRequest, + requestBody: unknown, +): Promise { + const { key, value } = requestBody as { key: string; value: string }; + + if (!request.session) { + return Response.json( + { + success: false, + code: 403, + error: "Unauthorized", + }, + { status: 403 }, + ); + } + + if (!request.session.roles.includes("admin")) { + return Response.json( + { + success: false, + code: 403, + error: "Unauthorized", + }, + { status: 403 }, + ); + } + + if (!key || !value) { + return Response.json( + { + success: false, + code: 400, + error: "Expected key and value", + }, + { status: 400 }, + ); + } + + if ( + typeof key !== "string" || + (typeof value !== "string" && + typeof value !== "boolean" && + typeof value !== "number") + ) { + return Response.json( + { + success: false, + code: 400, + error: "Expected key to be a string and value to be a string, boolean, or number", + }, + { status: 400 }, + ); + } + + try { + await setSetting(key, value); + } catch (error) { + logger.error(["Could not set the setting:", error as Error]); + + return Response.json( + { + success: false, + code: 500, + error: "Failed to set setting", + }, + { status: 500 }, + ); + } + + return Response.json( + { + success: true, + code: 200, + message: "Setting set", + }, + { status: 200 }, + ); +} + +export { handler, routeDef }; diff --git a/src/server.ts b/src/server.ts index b3eb107..0c3b4b2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -23,6 +23,7 @@ class ServerHandler { this.router = new FileSystemRouter({ style: "nextjs", dir: "./src/routes", + fileExtensions: [".ts"], origin: `http://${this.host}:${this.port}`, }); } diff --git a/types/config.d.ts b/types/config.d.ts index 86a49f6..a265432 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -8,3 +8,8 @@ type UserValidation = { check: { valid: boolean; error?: string }; field: string; }; + +type Setting = { + key: string; + value: string; +}; diff --git a/types/session.d.ts b/types/session.d.ts index fa042a7..02415a4 100644 --- a/types/session.d.ts +++ b/types/session.d.ts @@ -21,7 +21,7 @@ type User = { email_verified: boolean; password: string; avatar: boolean; - roles: string[]; + roles: string; timezone: string; invited_by: UUID; created_at: Date;