diff --git a/config.example.yml b/config.example.yml index 5f39206..d0cbcec 100644 --- a/config.example.yml +++ b/config.example.yml @@ -25,9 +25,10 @@ gerbil: block_size: 16 subnet_group: 10.0.0.0/8 -rate_limit: - window_minutes: 1 - max_requests: 100 +rate_limits: + global: + window_minutes: 1 + max_requests: 100 email: smtp_host: host.hoster.net diff --git a/server/apiServer.ts b/server/apiServer.ts index a72fa3e..8fc131a 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -38,9 +38,9 @@ export function createApiServer() { if (!dev) { apiServer.use( rateLimitMiddleware({ - windowMin: config.rate_limit.window_minutes, - max: config.rate_limit.max_requests, - type: "IP_ONLY", + windowMin: config.rate_limits.global.window_minutes, + max: config.rate_limits.global.max_requests, + type: "IP_AND_PATH", }), ); } diff --git a/server/routers/auth/sendEmailVerificationCode.ts b/server/auth/sendEmailVerificationCode.ts similarity index 100% rename from server/routers/auth/sendEmailVerificationCode.ts rename to server/auth/sendEmailVerificationCode.ts diff --git a/server/config.ts b/server/config.ts index 916c05f..98f6992 100644 --- a/server/config.ts +++ b/server/config.ts @@ -16,7 +16,7 @@ const environmentSchema = z.object({ app: z.object({ base_url: z.string().url(), log_level: z.enum(["debug", "info", "warn", "error"]), - save_logs: z.boolean(), + save_logs: z.boolean() }), server: z.object({ external_port: portSchema, @@ -26,24 +26,32 @@ const environmentSchema = z.object({ secure_cookies: z.boolean(), signup_secret: z.string().optional(), session_cookie_name: z.string(), - resource_session_cookie_name: z.string(), + resource_session_cookie_name: z.string() }), traefik: z.object({ http_entrypoint: z.string(), https_entrypoint: z.string().optional(), cert_resolver: z.string().optional(), - prefer_wildcard_cert: z.boolean().optional(), + prefer_wildcard_cert: z.boolean().optional() }), gerbil: z.object({ start_port: portSchema, base_endpoint: z.string(), use_subdomain: z.boolean(), subnet_group: z.string(), - block_size: z.number().positive().gt(0), + block_size: z.number().positive().gt(0) }), - rate_limit: z.object({ - window_minutes: z.number().positive().gt(0), - max_requests: z.number().positive().gt(0), + rate_limits: z.object({ + global: z.object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }), + auth: z + .object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }) + .optional() }), email: z .object({ @@ -51,7 +59,7 @@ const environmentSchema = z.object({ smtp_port: portSchema.optional(), smtp_user: z.string().optional(), smtp_pass: z.string().optional(), - no_reply: z.string().email().optional(), + no_reply: z.string().email().optional() }) .optional(), flags: z @@ -59,9 +67,9 @@ const environmentSchema = z.object({ allow_org_subdomain_changing: z.boolean().optional(), require_email_verification: z.boolean().optional(), disable_signup_without_invite: z.boolean().optional(), - require_signup_secret: z.boolean().optional(), + require_signup_secret: z.boolean().optional() }) - .optional(), + .optional() }); const loadConfig = (configPath: string) => { @@ -72,7 +80,7 @@ const loadConfig = (configPath: string) => { } catch (error) { if (error instanceof Error) { throw new Error( - `Error loading configuration file: ${error.message}`, + `Error loading configuration file: ${error.message}` ); } throw error; @@ -94,21 +102,21 @@ if (!environment) { try { const exampleConfigContent = fs.readFileSync( exampleConfigPath, - "utf8", + "utf8" ); fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8"); environment = loadConfig(configFilePath1); } catch (error) { if (error instanceof Error) { throw new Error( - `Error creating configuration file from example: ${error.message}`, + `Error creating configuration file from example: ${error.message}` ); } throw error; } } else { throw new Error( - "No configuration file found and no example configuration available", + "No configuration file found and no example configuration available" ); } } diff --git a/server/routers/accessToken/deleteAccessToken.ts b/server/routers/accessToken/deleteAccessToken.ts index 67223f2..891ede0 100644 --- a/server/routers/accessToken/deleteAccessToken.ts +++ b/server/routers/accessToken/deleteAccessToken.ts @@ -9,9 +9,11 @@ import { resourceAccessToken } from "@server/db/schema"; import { and, eq } from "drizzle-orm"; import db from "@server/db"; -const deleteAccessTokenParamsSchema = z.object({ - accessTokenId: z.string() -}); +const deleteAccessTokenParamsSchema = z + .object({ + accessTokenId: z.string() + }) + .strict(); export async function deleteAccessToken( req: Request, diff --git a/server/routers/accessToken/generateAccessToken.ts b/server/routers/accessToken/generateAccessToken.ts index 4f70639..b1b5a58 100644 --- a/server/routers/accessToken/generateAccessToken.ts +++ b/server/routers/accessToken/generateAccessToken.ts @@ -5,7 +5,11 @@ import { SESSION_COOKIE_EXPIRES } from "@server/auth"; import db from "@server/db"; -import { ResourceAccessToken, resourceAccessToken, resources } from "@server/db/schema"; +import { + ResourceAccessToken, + resourceAccessToken, + resources +} from "@server/db/schema"; import HttpCode from "@server/types/HttpCode"; import response from "@server/utils/response"; import { eq } from "drizzle-orm"; @@ -16,17 +20,27 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { createDate, TimeSpan } from "oslo"; -export const generateAccessTokenBodySchema = z.object({ - validForSeconds: z.number().int().positive().optional(), // seconds - title: z.string().optional(), - description: z.string().optional() -}); +export const generateAccessTokenBodySchema = z + .object({ + validForSeconds: z.number().int().positive().optional(), // seconds + title: z.string().optional(), + description: z.string().optional() + }) + .strict(); -export const generateAccssTokenParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) -}); +export const generateAccssTokenParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); -export type GenerateAccessTokenResponse = ResourceAccessToken; +export type GenerateAccessTokenResponse = Omit< + ResourceAccessToken, + "tokenHash" +> & { accessToken: string }; export async function generateAccessToken( req: Request, @@ -77,25 +91,38 @@ export async function generateAccessToken( const token = generateIdFromEntropySize(25); - // const tokenHash = await hash(token, { - // memoryCost: 19456, - // timeCost: 2, - // outputLen: 32, - // parallelism: 1 - // }); + const tokenHash = await hash(token, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); const id = generateId(15); - const [result] = await db.insert(resourceAccessToken).values({ - accessTokenId: id, - orgId: resource.orgId, - resourceId, - tokenHash: token, - expiresAt: expiresAt || null, - sessionLength: sessionLength, - title: title || null, - description: description || null, - createdAt: new Date().getTime() - }).returning(); + const [result] = await db + .insert(resourceAccessToken) + .values({ + accessTokenId: id, + orgId: resource.orgId, + resourceId, + tokenHash, + expiresAt: expiresAt || null, + sessionLength: sessionLength, + title: title || null, + description: description || null, + createdAt: new Date().getTime() + }) + .returning({ + accessTokenId: resourceAccessToken.accessTokenId, + orgId: resourceAccessToken.orgId, + resourceId: resourceAccessToken.resourceId, + expiresAt: resourceAccessToken.expiresAt, + sessionLength: resourceAccessToken.sessionLength, + title: resourceAccessToken.title, + description: resourceAccessToken.description, + createdAt: resourceAccessToken.createdAt + }) + .execute(); if (!result) { return next( @@ -107,7 +134,7 @@ export async function generateAccessToken( } return response(res, { - data: result, + data: { ...result, accessToken: token }, success: true, error: false, message: "Resource access token generated successfully", diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts index 2b5de64..5ef543d 100644 --- a/server/routers/accessToken/listAccessTokens.ts +++ b/server/routers/accessToken/listAccessTokens.ts @@ -23,6 +23,7 @@ const listAccessTokensParamsSchema = z .pipe(z.number().int().positive().optional()), orgId: z.string().optional() }) + .strict() .refine((data) => !!data.resourceId !== !!data.orgId, { message: "Either resourceId or orgId must be provided, but not both" }); @@ -65,7 +66,10 @@ function queryAccessTokens( return db .select(cols) .from(resourceAccessToken) - .leftJoin(resources, eq(resourceAccessToken.resourceId, resources.resourceId)) + .leftJoin( + resources, + eq(resourceAccessToken.resourceId, resources.resourceId) + ) .where( and( inArray( @@ -83,7 +87,10 @@ function queryAccessTokens( return db .select(cols) .from(resourceAccessToken) - .leftJoin(resources, eq(resourceAccessToken.resourceId, resources.resourceId)) + .leftJoin( + resources, + eq(resourceAccessToken.resourceId, resources.resourceId) + ) .where( and( inArray( diff --git a/server/routers/auth/changePassword.ts b/server/routers/auth/changePassword.ts index aff5d3e..ecf3789 100644 --- a/server/routers/auth/changePassword.ts +++ b/server/routers/auth/changePassword.ts @@ -11,12 +11,13 @@ import { response } from "@server/utils"; import { hashPassword, verifyPassword } from "@server/auth/password"; import { verifyTotpCode } from "@server/auth/2fa"; import { passwordSchema } from "@server/auth/passwordSchema"; +import logger from "@server/logger"; export const changePasswordBody = z.object({ oldPassword: z.string(), newPassword: passwordSchema, code: z.string().optional(), -}); +}).strict(); export type ChangePasswordBody = z.infer; @@ -108,6 +109,7 @@ export async function changePassword( status: HttpCode.OK, }); } catch (error) { + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts index c5f453f..95a27e2 100644 --- a/server/routers/auth/checkResourceSession.ts +++ b/server/routers/auth/checkResourceSession.ts @@ -5,11 +5,12 @@ import { fromError } from "zod-validation-error"; import HttpCode from "@server/types/HttpCode"; import { response } from "@server/utils"; import { validateResourceSessionToken } from "@server/auth/resource"; +import logger from "@server/logger"; export const params = z.object({ token: z.string(), resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +}).strict(); export type CheckResourceSessionParams = z.infer; @@ -54,6 +55,7 @@ export async function checkResourceSession( status: HttpCode.OK, }); } catch (e) { + logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/disable2fa.ts b/server/routers/auth/disable2fa.ts index f85bd67..05ed633 100644 --- a/server/routers/auth/disable2fa.ts +++ b/server/routers/auth/disable2fa.ts @@ -10,11 +10,12 @@ import { eq } from "drizzle-orm"; import { response } from "@server/utils"; import { verifyPassword } from "@server/auth/password"; import { verifyTotpCode } from "@server/auth/2fa"; +import logger from "@server/logger"; export const disable2faBody = z.object({ password: z.string(), code: z.string().optional(), -}); +}).strict(); export type Disable2faBody = z.infer; @@ -100,6 +101,7 @@ export async function disable2fa( status: HttpCode.OK, }); } catch (error) { + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index b7b68e4..8a0abdb 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -22,7 +22,7 @@ export const loginBodySchema = z.object({ email: z.string().email(), password: z.string(), code: z.string().optional(), -}); +}).strict(); export type LoginBody = z.infer; @@ -151,6 +151,7 @@ export async function login( status: HttpCode.OK, }); } catch (e) { + logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/logout.ts b/server/routers/auth/logout.ts index e5966e4..840ef2b 100644 --- a/server/routers/auth/logout.ts +++ b/server/routers/auth/logout.ts @@ -6,13 +6,13 @@ import logger from "@server/logger"; import { createBlankSessionTokenCookie, invalidateSession, - SESSION_COOKIE_NAME, + SESSION_COOKIE_NAME } from "@server/auth"; export async function logout( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { const sessionId = req.cookies[SESSION_COOKIE_NAME]; @@ -20,8 +20,8 @@ export async function logout( return next( createHttpError( HttpCode.BAD_REQUEST, - "You must be logged in to sign out", - ), + "You must be logged in to sign out" + ) ); } @@ -34,15 +34,12 @@ export async function logout( success: true, error: false, message: "Logged out successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { - logger.error("Failed to log out", error); + logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to log out", - ), + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to log out") ); } } diff --git a/server/routers/auth/requestEmailVerificationCode.ts b/server/routers/auth/requestEmailVerificationCode.ts index 7aab3c7..a5b19b5 100644 --- a/server/routers/auth/requestEmailVerificationCode.ts +++ b/server/routers/auth/requestEmailVerificationCode.ts @@ -3,8 +3,9 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { response } from "@server/utils"; import { User } from "@server/db/schema"; -import { sendEmailVerificationCode } from "./sendEmailVerificationCode"; +import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode"; import config from "@server/config"; +import logger from "@server/logger"; export type RequestEmailVerificationCodeResponse = { codeSent: boolean; @@ -40,14 +41,15 @@ export async function requestEmailVerificationCode( return response(res, { data: { - codeSent: true, + codeSent: true }, status: HttpCode.OK, success: true, error: false, - message: `Email verification code sent to ${user.email}`, + message: `Email verification code sent to ${user.email}` }); } catch (error) { + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/requestPasswordReset.ts b/server/routers/auth/requestPasswordReset.ts index e2cdb48..d0e672b 100644 --- a/server/routers/auth/requestPasswordReset.ts +++ b/server/routers/auth/requestPasswordReset.ts @@ -16,7 +16,7 @@ import { TimeSpan } from "oslo"; export const requestPasswordResetBody = z.object({ email: z.string().email(), -}); +}).strict(); export type RequestPasswordResetBody = z.infer; @@ -87,6 +87,7 @@ export async function requestPasswordReset( status: HttpCode.OK, }); } catch (e) { + logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/requestTotpSecret.ts b/server/routers/auth/requestTotpSecret.ts index 343393d..473d77d 100644 --- a/server/routers/auth/requestTotpSecret.ts +++ b/server/routers/auth/requestTotpSecret.ts @@ -12,10 +12,13 @@ import { eq } from "drizzle-orm"; import { verify } from "@node-rs/argon2"; import { createTOTPKeyURI } from "oslo/otp"; import config from "@server/config"; +import logger from "@server/logger"; -export const requestTotpSecretBody = z.object({ - password: z.string(), -}); +export const requestTotpSecretBody = z + .object({ + password: z.string() + }) + .strict(); export type RequestTotpSecretBody = z.infer; @@ -26,7 +29,7 @@ export type RequestTotpSecretResponse = { export async function requestTotpSecret( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { const parsedBody = requestTotpSecretBody.safeParse(req.body); @@ -34,8 +37,8 @@ export async function requestTotpSecret( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString(), - ), + fromError(parsedBody.error).toString() + ) ); } @@ -48,7 +51,7 @@ export async function requestTotpSecret( memoryCost: 19456, timeCost: 2, outputLen: 32, - parallelism: 1, + parallelism: 1 }); if (!validPassword) { return next(unauthorized()); @@ -58,8 +61,8 @@ export async function requestTotpSecret( return next( createHttpError( HttpCode.BAD_REQUEST, - "User has already enabled two-factor authentication", - ), + "User has already enabled two-factor authentication" + ) ); } @@ -70,25 +73,26 @@ export async function requestTotpSecret( await db .update(users) .set({ - twoFactorSecret: secret, + twoFactorSecret: secret }) .where(eq(users.userId, user.userId)); return response(res, { data: { - secret: uri, + secret: uri }, success: true, error: false, message: "TOTP secret generated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "Failed to generate TOTP secret", - ), + "Failed to generate TOTP secret" + ) ); } } diff --git a/server/routers/auth/resetPassword.ts b/server/routers/auth/resetPassword.ts index 0d12da2..020fabf 100644 --- a/server/routers/auth/resetPassword.ts +++ b/server/routers/auth/resetPassword.ts @@ -14,12 +14,15 @@ import { passwordSchema } from "@server/auth/passwordSchema"; import { encodeHex } from "oslo/encoding"; import { isWithinExpirationDate } from "oslo"; import { invalidateAllSessions } from "@server/auth"; +import logger from "@server/logger"; -export const resetPasswordBody = z.object({ - token: z.string(), - newPassword: passwordSchema, - code: z.string().optional(), -}); +export const resetPasswordBody = z + .object({ + token: z.string(), + newPassword: passwordSchema, + code: z.string().optional() + }) + .strict(); export type ResetPasswordBody = z.infer; @@ -30,7 +33,7 @@ export type ResetPasswordResponse = { export async function resetPassword( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { const parsedBody = resetPasswordBody.safeParse(req.body); @@ -38,8 +41,8 @@ export async function resetPassword( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString(), - ), + fromError(parsedBody.error).toString() + ) ); } @@ -47,7 +50,7 @@ export async function resetPassword( try { const tokenHash = encodeHex( - await sha256(new TextEncoder().encode(token)), + await sha256(new TextEncoder().encode(token)) ); const resetRequest = await db @@ -63,8 +66,8 @@ export async function resetPassword( return next( createHttpError( HttpCode.BAD_REQUEST, - "Invalid or expired password reset token", - ), + "Invalid or expired password reset token" + ) ); } @@ -77,8 +80,8 @@ export async function resetPassword( return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "User not found", - ), + "User not found" + ) ); } @@ -89,22 +92,22 @@ export async function resetPassword( success: true, error: false, message: "Two-factor authentication required", - status: HttpCode.ACCEPTED, + status: HttpCode.ACCEPTED }); } const validOTP = await verifyTotpCode( code!, user[0].twoFactorSecret!, - user[0].userId, + user[0].userId ); if (!validOTP) { return next( createHttpError( HttpCode.BAD_REQUEST, - "Invalid two-factor authentication code", - ), + "Invalid two-factor authentication code" + ) ); } } @@ -129,14 +132,15 @@ export async function resetPassword( success: true, error: false, message: "Password reset successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (e) { + logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "Failed to reset password", - ), + "Failed to reset password" + ) ); } } diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 536d9c1..8f332c2 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -8,7 +8,7 @@ import { fromError } from "zod-validation-error"; import createHttpError from "http-errors"; import response from "@server/utils/response"; import { SqliteError } from "better-sqlite3"; -import { sendEmailVerificationCode } from "./sendEmailVerificationCode"; +import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode"; import { passwordSchema } from "@server/auth/passwordSchema"; import { eq } from "drizzle-orm"; import moment from "moment"; @@ -20,6 +20,7 @@ import { } from "@server/auth"; import { ActionsEnum } from "@server/auth/actions"; import config from "@server/config"; +import logger from "@server/logger"; export const signupBodySchema = z.object({ email: z.string().email(), @@ -153,6 +154,7 @@ export async function signup( ) ); } else { + logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts index 6df3d48..59525a0 100644 --- a/server/routers/auth/verifyEmail.ts +++ b/server/routers/auth/verifyEmail.ts @@ -9,10 +9,13 @@ import { User, emailVerificationCodes, users } from "@server/db/schema"; import { eq } from "drizzle-orm"; import { isWithinExpirationDate } from "oslo"; import config from "@server/config"; +import logger from "@server/logger"; -export const verifyEmailBody = z.object({ - code: z.string(), -}); +export const verifyEmailBody = z + .object({ + code: z.string() + }) + .strict(); export type VerifyEmailBody = z.infer; @@ -66,7 +69,7 @@ export async function verifyEmail( await db .update(users) .set({ - emailVerified: true, + emailVerified: true }) .where(eq(users.userId, user.userId)); } else { @@ -84,10 +87,11 @@ export async function verifyEmail( message: "Email verified", status: HttpCode.OK, data: { - valid, - }, + valid + } }); } catch (error) { + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index 341c668..9f54dab 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -10,10 +10,13 @@ import { eq } from "drizzle-orm"; import { alphabet, generateRandomString } from "oslo/crypto"; import { hashPassword } from "@server/auth/password"; import { verifyTotpCode } from "@server/auth/2fa"; +import logger from "@server/logger"; -export const verifyTotpBody = z.object({ - code: z.string(), -}); +export const verifyTotpBody = z + .object({ + code: z.string() + }) + .strict(); export type VerifyTotpBody = z.infer; @@ -82,7 +85,7 @@ export async function verifyTotp( await db.insert(twoFactorBackupCodes).values({ userId: user.userId, - codeHash: hash, + codeHash: hash }); } } @@ -92,16 +95,17 @@ export async function verifyTotp( return response(res, { data: { valid, - ...(valid && codes ? { backupCodes: codes } : {}), + ...(valid && codes ? { backupCodes: codes } : {}) }, success: true, error: false, message: valid ? "Code is valid. Two-factor is now enabled" : "Code is invalid", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index fadcd35..734a233 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -139,39 +139,36 @@ export async function verifyResourceSession( ); if (resourceSession) { - return allowed(res); + if (pincode && resourceSession.pincodeId) { + logger.debug( + "Resource allowed because pincode session is valid" + ); + return allowed(res); + } - // Might not be needed - // if (pincode && resourceSession.pincodeId) { - // logger.debug( - // "Resource allowed because pincode session is valid" - // ); - // return allowed(res); - // } - // - // if (password && resourceSession.passwordId) { - // logger.debug( - // "Resource allowed because password session is valid" - // ); - // return allowed(res); - // } - // - // if ( - // resource.emailWhitelistEnabled && - // resourceSession.whitelistId - // ) { - // logger.debug( - // "Resource allowed because whitelist session is valid" - // ); - // return allowed(res); - // } - // - // if (resourceSession.accessTokenId) { - // logger.debug( - // "Resource allowed because access token session is valid" - // ); - // return allowed(res); - // } + if (password && resourceSession.passwordId) { + logger.debug( + "Resource allowed because password session is valid" + ); + return allowed(res); + } + + if ( + resource.emailWhitelistEnabled && + resourceSession.whitelistId + ) { + logger.debug( + "Resource allowed because whitelist session is valid" + ); + return allowed(res); + } + + if (resourceSession.accessTokenId) { + logger.debug( + "Resource allowed because access token session is valid" + ); + return allowed(res); + } } } diff --git a/server/routers/external.ts b/server/routers/external.ts index 70df872..e504f23 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -1,4 +1,5 @@ import { Router } from "express"; +import config from "@server/config"; import * as site from "./site"; import * as org from "./org"; import * as resource from "./resource"; @@ -419,8 +420,12 @@ export const authRouter = Router(); unauthenticated.use("/auth", authRouter); authRouter.use( rateLimitMiddleware({ - windowMin: 10, - max: 75, + windowMin: + config.rate_limits.auth?.window_minutes || + config.rate_limits.global.window_minutes, + max: + config.rate_limits.auth?.max_requests || + config.rate_limits.global.max_requests, type: "IP_AND_PATH" }) ); diff --git a/server/routers/org/checkId.ts b/server/routers/org/checkId.ts index 7ab2fe5..86ac0ee 100644 --- a/server/routers/org/checkId.ts +++ b/server/routers/org/checkId.ts @@ -9,9 +9,11 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const getOrgSchema = z.object({ - orgId: z.string(), -}); +const getOrgSchema = z + .object({ + orgId: z.string() + }) + .strict(); export async function checkId( req: Request, @@ -43,7 +45,7 @@ export async function checkId( success: true, error: false, message: "Organization ID already exists", - status: HttpCode.OK, + status: HttpCode.OK }); } @@ -52,7 +54,7 @@ export async function checkId( success: true, error: false, message: "Organization ID is available", - status: HttpCode.NOT_FOUND, + status: HttpCode.NOT_FOUND }); } catch (error) { logger.error(error); diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index 5bbecb9..8e5253b 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -10,9 +10,11 @@ import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const deleteOrgSchema = z.object({ - orgId: z.string(), -}); +const deleteOrgSchema = z + .object({ + orgId: z.string() + }) + .strict(); export async function deleteOrg( req: Request, @@ -65,7 +67,7 @@ export async function deleteOrg( success: true, error: false, message: "Organization deleted successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index 97ace60..18cf2ec 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -8,9 +8,11 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -const getOrgSchema = z.object({ - orgId: z.string(), -}); +const getOrgSchema = z + .object({ + orgId: z.string() + }) + .strict(); export type GetOrgResponse = { org: Org; @@ -51,12 +53,12 @@ export async function getOrg( return response(res, { data: { - org: org[0], + org: org[0] }, success: true, error: false, message: "Organization retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 4e70061..24741d2 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -9,18 +9,20 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const updateOrgParamsSchema = z.object({ - orgId: z.string(), -}); +const updateOrgParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); const updateOrgBodySchema = z .object({ name: z.string().min(1).max(255).optional(), - domain: z.string().min(1).max(255).optional(), + domain: z.string().min(1).max(255).optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update", + message: "At least one field must be provided for update" }); export async function updateOrg( @@ -72,7 +74,7 @@ export async function updateOrg( success: true, error: false, message: "Organization updated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/resource/authWithAccessToken.ts b/server/routers/resource/authWithAccessToken.ts index 5e43a04..0ca1084 100644 --- a/server/routers/resource/authWithAccessToken.ts +++ b/server/routers/resource/authWithAccessToken.ts @@ -17,13 +17,21 @@ import logger from "@server/logger"; import { verify } from "@node-rs/argon2"; import { isWithinExpirationDate } from "oslo"; -const authWithAccessTokenBodySchema = z.object({ - accessToken: z.string() -}); +const authWithAccessTokenBodySchema = z + .object({ + accessToken: z.string(), + accessTokenId: z.string() + }) + .strict(); -const authWithAccessTokenParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) -}); +const authWithAccessTokenParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export type AuthWithAccessTokenResponse = { session?: string; @@ -57,9 +65,7 @@ export async function authWithAccessToken( } const { resourceId } = parsedParams.data; - const { accessToken: at } = parsedBody.data; - - const [accessTokenId, accessToken] = at.split("."); + const { accessToken, accessTokenId } = parsedBody.data; try { const [result] = await db @@ -86,7 +92,7 @@ export async function authWithAccessToken( HttpCode.UNAUTHORIZED, createHttpError( HttpCode.BAD_REQUEST, - "Email is not whitelisted" + "Access token does not exist for resource" ) ) ); @@ -98,15 +104,12 @@ export async function authWithAccessToken( ); } - // const validCode = await verify(tokenItem.tokenHash, accessToken, { - // memoryCost: 19456, - // timeCost: 2, - // outputLen: 32, - // parallelism: 1 - // }); - logger.debug(`${accessToken} ${tokenItem.tokenHash}`) - const validCode = accessToken === tokenItem.tokenHash; - + const validCode = await verify(tokenItem.tokenHash, accessToken, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); if (!validCode) { return next( createHttpError(HttpCode.UNAUTHORIZED, "Invalid access token") diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index 65538dc..ba1c73f 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -14,14 +14,22 @@ import { serializeResourceSessionCookie } from "@server/auth/resource"; import config from "@server/config"; +import logger from "@server/logger"; -export const authWithPasswordBodySchema = z.object({ - password: z.string() -}); +export const authWithPasswordBodySchema = z + .object({ + password: z.string() + }) + .strict(); -export const authWithPasswordParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) -}); +export const authWithPasswordParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export type AuthWithPasswordResponse = { session?: string; @@ -120,10 +128,7 @@ export async function authWithPassword( passwordId: definedPassword.passwordId }); const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; - const cookie = serializeResourceSessionCookie( - cookieName, - token, - ); + const cookie = serializeResourceSessionCookie(cookieName, token); res.appendHeader("Set-Cookie", cookie); return response(res, { @@ -136,6 +141,7 @@ export async function authWithPassword( status: HttpCode.OK }); } catch (e) { + logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 45b46e3..f57629c 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -24,13 +24,20 @@ import config from "@server/config"; import { AuthWithPasswordResponse } from "./authWithPassword"; import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp"; -export const authWithPincodeBodySchema = z.object({ - pincode: z.string() -}); +export const authWithPincodeBodySchema = z + .object({ + pincode: z.string() + }) + .strict(); -export const authWithPincodeParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) -}); +export const authWithPincodeParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export type AuthWithPincodeResponse = { session?: string; @@ -128,10 +135,7 @@ export async function authWithPincode( pincodeId: definedPincode.pincodeId }); const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; - const cookie = serializeResourceSessionCookie( - cookieName, - token, - ); + const cookie = serializeResourceSessionCookie(cookieName, token); res.appendHeader("Set-Cookie", cookie); return response(res, { diff --git a/server/routers/resource/authWithWhitelist.ts b/server/routers/resource/authWithWhitelist.ts index c5b6a56..3f490ee 100644 --- a/server/routers/resource/authWithWhitelist.ts +++ b/server/routers/resource/authWithWhitelist.ts @@ -22,14 +22,21 @@ import config from "@server/config"; import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp"; import logger from "@server/logger"; -const authWithWhitelistBodySchema = z.object({ - email: z.string().email(), - otp: z.string().optional() -}); +const authWithWhitelistBodySchema = z + .object({ + email: z.string().email(), + otp: z.string().optional() + }) + .strict(); -const authWithWhitelistParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) -}); +const authWithWhitelistParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export type AuthWithWhitelistResponse = { otpSent?: boolean; @@ -171,10 +178,7 @@ export async function authWithWhitelist( whitelistId: whitelistedEmail.whitelistId }); const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; - const cookie = serializeResourceSessionCookie( - cookieName, - token, - ); + const cookie = serializeResourceSessionCookie(cookieName, token); res.appendHeader("Set-Cookie", cookie); return response(res, { diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 002e543..018faf2 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -1,3 +1,4 @@ +import { SqliteError } from "better-sqlite3"; import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; @@ -7,7 +8,7 @@ import { resources, roleResources, roles, - userResources, + userResources } from "@server/db/schema"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; @@ -16,16 +17,19 @@ import { eq, and } from "drizzle-orm"; import stoi from "@server/utils/stoi"; import { fromError } from "zod-validation-error"; import { subdomainSchema } from "@server/schemas/subdomainSchema"; +import logger from "@server/logger"; -const createResourceParamsSchema = z.object({ - siteId: z.string().transform(stoi).pipe(z.number().int().positive()), - orgId: z.string(), -}); +const createResourceParamsSchema = z + .object({ + siteId: z.string().transform(stoi).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); const createResourceSchema = z .object({ name: z.string().min(1).max(255), - subdomain: subdomainSchema, + subdomain: subdomainSchema }) .strict(); @@ -94,7 +98,7 @@ export async function createResource( orgId, name, subdomain, - ssl: true, + ssl: true }) .returning(); @@ -112,14 +116,14 @@ export async function createResource( await db.insert(roleResources).values({ roleId: adminRole[0].roleId, - resourceId: newResource[0].resourceId, + resourceId: newResource[0].resourceId }); if (req.userOrgRoleId != adminRole[0].roleId) { // make sure the user can access the resource await db.insert(userResources).values({ userId: req.user?.userId!, - resourceId: newResource[0].resourceId, + resourceId: newResource[0].resourceId }); } @@ -128,9 +132,22 @@ export async function createResource( success: true, error: false, message: "Resource created successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { + if ( + error instanceof SqliteError && + error.code === "SQLITE_CONSTRAINT_UNIQUE" + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that subdomain already exists" + ) + ); + } + + logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index d41440f..b4318fb 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -12,9 +12,14 @@ import { addPeer } from "../gerbil/peers"; import { removeTargets } from "../newt/targets"; // Define Zod schema for request parameters validation -const deleteResourceSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const deleteResourceSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export async function deleteResource( req: Request, @@ -67,20 +72,20 @@ export async function deleteResource( ) ); } - + if (site.pubKey) { if (site.type == "wireguard") { // TODO: is this all inefficient? // Fetch resources for this site const resourcesRes = await db.query.resources.findMany({ - where: eq(resources.siteId, site.siteId), + where: eq(resources.siteId, site.siteId) }); // Fetch targets for all resources of this site const targetIps = await Promise.all( resourcesRes.map(async (resource) => { const targetsRes = await db.query.targets.findMany({ - where: eq(targets.resourceId, resource.resourceId), + where: eq(targets.resourceId, resource.resourceId) }); return targetsRes.map((target) => `${target.ip}/32`); }) @@ -88,7 +93,7 @@ export async function deleteResource( await addPeer(site.exitNodeId!, { publicKey: site.pubKey, - allowedIps: targetIps.flat(), + allowedIps: targetIps.flat() }); } else if (site.type == "newt") { // get the newt on the site by querying the newt table for siteId @@ -107,7 +112,7 @@ export async function deleteResource( success: true, error: false, message: "Resource deleted successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 9705545..dfd48eb 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -7,10 +7,16 @@ import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; -const getResourceSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const getResourceSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export type GetResourceResponse = Resource; @@ -52,9 +58,10 @@ export async function getResource( success: true, error: false, message: "Resource retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { + logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 8c20e9c..10305e2 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -4,17 +4,23 @@ import { db } from "@server/db"; import { resourcePassword, resourcePincode, - resources, + resources } from "@server/db/schema"; import { eq } from "drizzle-orm"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; -const getResourceAuthInfoSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const getResourceAuthInfoSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export type GetResourceAuthInfoResponse = { resourceId: number; @@ -30,7 +36,7 @@ export type GetResourceAuthInfoResponse = { export async function getResourceAuthInfo( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { try { const parsedParams = getResourceAuthInfoSchema.safeParse(req.params); @@ -38,8 +44,8 @@ export async function getResourceAuthInfo( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString(), - ), + fromError(parsedParams.error).toString() + ) ); } @@ -50,11 +56,11 @@ export async function getResourceAuthInfo( .from(resources) .leftJoin( resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId), + eq(resourcePincode.resourceId, resources.resourceId) ) .leftJoin( resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId), + eq(resourcePassword.resourceId, resources.resourceId) ) .where(eq(resources.resourceId, resourceId)) .limit(1); @@ -67,7 +73,7 @@ export async function getResourceAuthInfo( if (!resource) { return next( - createHttpError(HttpCode.NOT_FOUND, "Resource not found"), + createHttpError(HttpCode.NOT_FOUND, "Resource not found") ); } @@ -85,14 +91,12 @@ export async function getResourceAuthInfo( success: true, error: false, message: "Resource auth info retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { + logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred", - ), + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } diff --git a/server/routers/resource/getResourceWhitelist.ts b/server/routers/resource/getResourceWhitelist.ts index 57cc490..64a421a 100644 --- a/server/routers/resource/getResourceWhitelist.ts +++ b/server/routers/resource/getResourceWhitelist.ts @@ -9,9 +9,14 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const getResourceWhitelistSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) -}); +const getResourceWhitelistSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); async function queryWhitelist(resourceId: number) { return await db diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts index ea9ebad..8b0069f 100644 --- a/server/routers/resource/listResourceRoles.ts +++ b/server/routers/resource/listResourceRoles.ts @@ -9,9 +9,14 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listResourceRolesSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const listResourceRolesSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); async function query(resourceId: number) { return await db @@ -19,7 +24,7 @@ async function query(resourceId: number) { roleId: roles.roleId, name: roles.name, description: roles.description, - isAdmin: roles.isAdmin, + isAdmin: roles.isAdmin }) .from(roleResources) .innerJoin(roles, eq(roleResources.roleId, roles.roleId)) @@ -52,12 +57,12 @@ export async function listResourceRoles( return response(res, { data: { - roles: resourceRolesList, + roles: resourceRolesList }, success: true, error: false, message: "Resource roles retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/resource/listResourceUsers.ts b/server/routers/resource/listResourceUsers.ts index e2fc9a7..d4b0182 100644 --- a/server/routers/resource/listResourceUsers.ts +++ b/server/routers/resource/listResourceUsers.ts @@ -9,15 +9,20 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listResourceUsersSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const listResourceUsersSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); async function queryUsers(resourceId: number) { return await db .select({ userId: userResources.userId, - email: users.email, + email: users.email }) .from(userResources) .innerJoin(users, eq(userResources.userId, users.userId)) @@ -50,12 +55,12 @@ export async function listResourceUsers( return response(res, { data: { - users: resourceUsersList, + users: resourceUsersList }, success: true, error: false, message: "Resource users retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 79bb064..0752fef 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -7,7 +7,7 @@ import { userResources, roleResources, resourcePassword, - resourcePincode, + resourcePincode } from "@server/db/schema"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; @@ -23,10 +23,11 @@ const listResourcesParamsSchema = z .optional() .transform(stoi) .pipe(z.number().int().positive().optional()), - orgId: z.string().optional(), + orgId: z.string().optional() }) + .strict() .refine((data) => !!data.siteId !== !!data.orgId, { - message: "Either siteId or orgId must be provided, but not both", + message: "Either siteId or orgId must be provided, but not both" }); const listResourcesSchema = z.object({ @@ -42,13 +43,13 @@ const listResourcesSchema = z.object({ .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.number().int().nonnegative()) }); function queryResources( accessibleResourceIds: number[], siteId?: number, - orgId?: string, + orgId?: string ) { if (siteId) { return db @@ -68,17 +69,17 @@ function queryResources( .leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin( resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId), + eq(resourcePassword.resourceId, resources.resourceId) ) .leftJoin( resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId), + eq(resourcePincode.resourceId, resources.resourceId) ) .where( and( inArray(resources.resourceId, accessibleResourceIds), - eq(resources.siteId, siteId), - ), + eq(resources.siteId, siteId) + ) ); } else if (orgId) { return db @@ -98,17 +99,17 @@ function queryResources( .leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin( resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId), + eq(resourcePassword.resourceId, resources.resourceId) ) .leftJoin( resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId), + eq(resourcePincode.resourceId, resources.resourceId) ) .where( and( inArray(resources.resourceId, accessibleResourceIds), - eq(resources.orgId, orgId), - ), + eq(resources.orgId, orgId) + ) ); } } @@ -121,7 +122,7 @@ export type ListResourcesResponse = { export async function listResources( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { try { const parsedQuery = listResourcesSchema.safeParse(req.query); @@ -129,8 +130,8 @@ export async function listResources( return next( createHttpError( HttpCode.BAD_REQUEST, - parsedQuery.error.errors.map((e) => e.message).join(", "), - ), + parsedQuery.error.errors.map((e) => e.message).join(", ") + ) ); } const { limit, offset } = parsedQuery.data; @@ -140,8 +141,8 @@ export async function listResources( return next( createHttpError( HttpCode.BAD_REQUEST, - parsedParams.error.errors.map((e) => e.message).join(", "), - ), + parsedParams.error.errors.map((e) => e.message).join(", ") + ) ); } const { siteId, orgId } = parsedParams.data; @@ -150,29 +151,29 @@ export async function listResources( return next( createHttpError( HttpCode.FORBIDDEN, - "User does not have access to this organization", - ), + "User does not have access to this organization" + ) ); } const accessibleResources = await db .select({ - resourceId: sql`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`, + resourceId: sql`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})` }) .from(userResources) .fullJoin( roleResources, - eq(userResources.resourceId, roleResources.resourceId), + eq(userResources.resourceId, roleResources.resourceId) ) .where( or( eq(userResources.userId, req.user!.userId), - eq(roleResources.roleId, req.userOrgRoleId!), - ), + eq(roleResources.roleId, req.userOrgRoleId!) + ) ); const accessibleResourceIds = accessibleResources.map( - (resource) => resource.resourceId, + (resource) => resource.resourceId ); let countQuery: any = db @@ -192,21 +193,18 @@ export async function listResources( pagination: { total: totalCount, limit, - offset, - }, + offset + } }, success: true, error: false, message: "Resources retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred", - ), + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } diff --git a/server/routers/resource/setResourcePassword.ts b/server/routers/resource/setResourcePassword.ts index 085587f..81a4e1e 100644 --- a/server/routers/resource/setResourcePassword.ts +++ b/server/routers/resource/setResourcePassword.ts @@ -8,14 +8,15 @@ import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; import { hash } from "@node-rs/argon2"; import { response } from "@server/utils"; +import logger from "@server/logger"; const setResourceAuthMethodsParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), + resourceId: z.string().transform(Number).pipe(z.number().int().positive()) }); const setResourceAuthMethodsBodySchema = z .object({ - password: z.string().min(4).max(100).nullable(), + password: z.string().min(4).max(100).nullable() }) .strict(); @@ -60,7 +61,7 @@ export async function setResourcePassword( memoryCost: 19456, timeCost: 2, outputLen: 32, - parallelism: 1, + parallelism: 1 }); await trx @@ -74,9 +75,10 @@ export async function setResourcePassword( success: true, error: false, message: "Resource password set successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { + logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); diff --git a/server/routers/resource/setResourcePincode.ts b/server/routers/resource/setResourcePincode.ts index 865f7e8..da64a96 100644 --- a/server/routers/resource/setResourcePincode.ts +++ b/server/routers/resource/setResourcePincode.ts @@ -9,6 +9,7 @@ import { fromError } from "zod-validation-error"; import { hash } from "@node-rs/argon2"; import { response } from "@server/utils"; import stoi from "@server/utils/stoi"; +import logger from "@server/logger"; const setResourceAuthMethodsParamsSchema = z.object({ resourceId: z.string().transform(Number).pipe(z.number().int().positive()), @@ -81,6 +82,7 @@ export async function setResourcePincode( status: HttpCode.CREATED, }); } catch (error) { + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/routers/resource/setResourceRoles.ts b/server/routers/resource/setResourceRoles.ts index 818cb1f..4fb7e48 100644 --- a/server/routers/resource/setResourceRoles.ts +++ b/server/routers/resource/setResourceRoles.ts @@ -9,13 +9,20 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and, ne } from "drizzle-orm"; -const setResourceRolesBodySchema = z.object({ - roleIds: z.array(z.number().int().positive()), -}); +const setResourceRolesBodySchema = z + .object({ + roleIds: z.array(z.number().int().positive()) + }) + .strict(); -const setResourceRolesParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const setResourceRolesParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export async function setResourceRoles( req: Request, @@ -99,7 +106,7 @@ export async function setResourceRoles( success: true, error: false, message: "Roles set for resource successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); }); } catch (error) { diff --git a/server/routers/resource/setResourceUsers.ts b/server/routers/resource/setResourceUsers.ts index 795faba..5587582 100644 --- a/server/routers/resource/setResourceUsers.ts +++ b/server/routers/resource/setResourceUsers.ts @@ -9,13 +9,20 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq } from "drizzle-orm"; -const setUserResourcesBodySchema = z.object({ - userIds: z.array(z.string()), -}); +const setUserResourcesBodySchema = z + .object({ + userIds: z.array(z.string()) + }) + .strict(); -const setUserResourcesParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const setUserResourcesParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export async function setResourceUsers( req: Request, @@ -66,7 +73,7 @@ export async function setResourceUsers( success: true, error: false, message: "Users set for resource successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); }); } catch (error) { diff --git a/server/routers/resource/setResourceWhitelist.ts b/server/routers/resource/setResourceWhitelist.ts index 12bccdd..0ef24b0 100644 --- a/server/routers/resource/setResourceWhitelist.ts +++ b/server/routers/resource/setResourceWhitelist.ts @@ -9,13 +9,20 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; -const setResourceWhitelistBodySchema = z.object({ - emails: z.array(z.string().email()).max(50) -}); +const setResourceWhitelistBodySchema = z + .object({ + emails: z.array(z.string().email()).max(50) + }) + .strict(); -const setResourceWhitelistParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) -}); +const setResourceWhitelistParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export async function setResourceWhitelist( req: Request, diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index e51bb6f..da064e7 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -10,9 +10,14 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { subdomainSchema } from "@server/schemas/subdomainSchema"; -const updateResourceParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const updateResourceParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); const updateResourceBodySchema = z .object({ @@ -21,18 +26,18 @@ const updateResourceBodySchema = z ssl: z.boolean().optional(), sso: z.boolean().optional(), blockAccess: z.boolean().optional(), - emailWhitelistEnabled: z.boolean().optional(), + emailWhitelistEnabled: z.boolean().optional() // siteId: z.number(), }) .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update", + message: "At least one field must be provided for update" }); export async function updateResource( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { try { const parsedParams = updateResourceParamsSchema.safeParse(req.params); @@ -40,8 +45,8 @@ export async function updateResource( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString(), - ), + fromError(parsedParams.error).toString() + ) ); } @@ -50,8 +55,8 @@ export async function updateResource( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString(), - ), + fromError(parsedBody.error).toString() + ) ); } @@ -68,8 +73,8 @@ export async function updateResource( return next( createHttpError( HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found`, - ), + `Resource with ID ${resourceId} not found` + ) ); } @@ -77,8 +82,8 @@ export async function updateResource( return next( createHttpError( HttpCode.BAD_REQUEST, - "Resource does not have a domain", - ), + "Resource does not have a domain" + ) ); } @@ -88,7 +93,7 @@ export async function updateResource( const updatePayload = { ...updateData, - ...(fullDomain && { fullDomain }), + ...(fullDomain && { fullDomain }) }; const updatedResource = await db @@ -101,8 +106,8 @@ export async function updateResource( return next( createHttpError( HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found`, - ), + `Resource with ID ${resourceId} not found` + ) ); } @@ -111,15 +116,12 @@ export async function updateResource( success: true, error: false, message: "Resource updated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred", - ), + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } diff --git a/server/routers/role/addRoleAction.ts b/server/routers/role/addRoleAction.ts index 5f82522..1c3f2d2 100644 --- a/server/routers/role/addRoleAction.ts +++ b/server/routers/role/addRoleAction.ts @@ -9,13 +9,17 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addRoleActionParamSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const addRoleActionParamSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); -const addRoleActionSchema = z.object({ - actionId: z.string(), -}); +const addRoleActionSchema = z + .object({ + actionId: z.string() + }) + .strict(); export async function addRoleAction( req: Request, @@ -66,7 +70,7 @@ export async function addRoleAction( .values({ roleId, actionId, - orgId: role[0].orgId!, + orgId: role[0].orgId! }) .returning(); @@ -75,7 +79,7 @@ export async function addRoleAction( success: true, error: false, message: "Action added to role successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/server/routers/role/addRoleSite.ts b/server/routers/role/addRoleSite.ts index a326cb4..5204cae 100644 --- a/server/routers/role/addRoleSite.ts +++ b/server/routers/role/addRoleSite.ts @@ -9,13 +9,17 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addRoleSiteParamsSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const addRoleSiteParamsSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); -const addRoleSiteSchema = z.object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const addRoleSiteSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function addRoleSite( req: Request, @@ -51,7 +55,7 @@ export async function addRoleSite( .insert(roleSites) .values({ roleId, - siteId, + siteId }) .returning(); @@ -63,7 +67,7 @@ export async function addRoleSite( for (const resource of siteResources) { await db.insert(roleResources).values({ roleId, - resourceId: resource.resourceId, + resourceId: resource.resourceId }); } @@ -72,7 +76,7 @@ export async function addRoleSite( success: true, error: false, message: "Site added to role successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index b63deab..dd3656f 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -10,21 +10,23 @@ import { fromError } from "zod-validation-error"; import { ActionsEnum } from "@server/auth/actions"; import { eq, and } from "drizzle-orm"; -const createRoleParamsSchema = z.object({ - orgId: z.string(), -}); +const createRoleParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); const createRoleSchema = z .object({ name: z.string().min(1).max(255), - description: z.string().optional(), + description: z.string().optional() }) .strict(); export const defaultRoleAllowedActions: ActionsEnum[] = [ ActionsEnum.getOrg, ActionsEnum.getResource, - ActionsEnum.listResources, + ActionsEnum.listResources ]; export type CreateRoleBody = z.infer; @@ -64,7 +66,7 @@ export async function createRole( const allRoles = await db .select({ roleId: roles.roleId, - name: roles.name, + name: roles.name }) .from(roles) .leftJoin(orgs, eq(roles.orgId, orgs.orgId)) @@ -84,7 +86,7 @@ export async function createRole( .insert(roles) .values({ ...roleData, - orgId, + orgId }) .returning(); @@ -94,7 +96,7 @@ export async function createRole( defaultRoleAllowedActions.map((action) => ({ roleId: newRole[0].roleId, actionId: action, - orgId, + orgId })) ) .execute(); @@ -104,7 +106,7 @@ export async function createRole( success: true, error: false, message: "Role created successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts index 12f8ff8..1cc44b2 100644 --- a/server/routers/role/deleteRole.ts +++ b/server/routers/role/deleteRole.ts @@ -9,13 +9,17 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const deleteRoleSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const deleteRoleSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); -const deelteRoleBodySchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const deelteRoleBodySchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function deleteRole( req: Request, @@ -108,7 +112,7 @@ export async function deleteRole( success: true, error: false, message: "Role deleted successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/getRole.ts b/server/routers/role/getRole.ts index 7561708..54ce307 100644 --- a/server/routers/role/getRole.ts +++ b/server/routers/role/getRole.ts @@ -9,9 +9,11 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const getRoleSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const getRoleSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function getRole( req: Request, @@ -51,7 +53,7 @@ export async function getRole( success: true, error: false, message: "Role retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/listRoleActions.ts b/server/routers/role/listRoleActions.ts index b368a12..d33b971 100644 --- a/server/routers/role/listRoleActions.ts +++ b/server/routers/role/listRoleActions.ts @@ -9,9 +9,11 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listRoleActionsSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const listRoleActionsSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function listRoleActions( req: Request, @@ -35,7 +37,7 @@ export async function listRoleActions( .select({ actionId: actions.actionId, name: actions.name, - description: actions.description, + description: actions.description }) .from(roleActions) .innerJoin(actions, eq(roleActions.actionId, actions.actionId)) @@ -48,7 +50,7 @@ export async function listRoleActions( success: true, error: false, message: "Role actions retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/listRoleResources.ts b/server/routers/role/listRoleResources.ts index 5d1f13a..06405c3 100644 --- a/server/routers/role/listRoleResources.ts +++ b/server/routers/role/listRoleResources.ts @@ -9,9 +9,11 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listRoleResourcesSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const listRoleResourcesSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function listRoleResources( req: Request, @@ -35,7 +37,7 @@ export async function listRoleResources( .select({ resourceId: resources.resourceId, name: resources.name, - subdomain: resources.subdomain, + subdomain: resources.subdomain }) .from(roleResources) .innerJoin( @@ -51,7 +53,7 @@ export async function listRoleResources( success: true, error: false, message: "Role resources retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/listRoleSites.ts b/server/routers/role/listRoleSites.ts index 878304c..999dc54 100644 --- a/server/routers/role/listRoleSites.ts +++ b/server/routers/role/listRoleSites.ts @@ -9,9 +9,11 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listRoleSitesSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const listRoleSitesSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function listRoleSites( req: Request, @@ -34,7 +36,7 @@ export async function listRoleSites( const roleSitesList = await db .select({ siteId: sites.siteId, - name: sites.name, + name: sites.name }) .from(roleSites) .innerJoin(sites, eq(roleSites.siteId, sites.siteId)) @@ -47,7 +49,7 @@ export async function listRoleSites( success: true, error: false, message: "Role sites retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index 17cd169..c4c6f20 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -10,9 +10,11 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import stoi from "@server/utils/stoi"; -const listRolesParamsSchema = z.object({ - orgId: z.string(), -}); +const listRolesParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); const listRolesSchema = z.object({ limit: z @@ -26,7 +28,7 @@ const listRolesSchema = z.object({ .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.number().int().nonnegative()) }); async function queryRoles(orgId: string, limit: number, offset: number) { @@ -37,7 +39,7 @@ async function queryRoles(orgId: string, limit: number, offset: number) { isAdmin: roles.isAdmin, name: roles.name, description: roles.description, - orgName: orgs.name, + orgName: orgs.name }) .from(roles) .leftJoin(orgs, eq(roles.orgId, orgs.orgId)) @@ -100,13 +102,13 @@ export async function listRoles( pagination: { total: totalCount, limit, - offset, - }, + offset + } }, success: true, error: false, message: "Roles retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/removeRoleAction.ts b/server/routers/role/removeRoleAction.ts index ecf57a5..47bfc0e 100644 --- a/server/routers/role/removeRoleAction.ts +++ b/server/routers/role/removeRoleAction.ts @@ -9,13 +9,17 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeRoleActionParamsSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const removeRoleActionParamsSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); -const removeRoleActionSchema = z.object({ - actionId: z.string(), -}); +const removeRoleActionSchema = z + .object({ + actionId: z.string() + }) + .strict(); export async function removeRoleAction( req: Request, @@ -71,7 +75,7 @@ export async function removeRoleAction( success: true, error: false, message: "Action removed from role successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/removeRoleResource.ts b/server/routers/role/removeRoleResource.ts index 055c301..6724780 100644 --- a/server/routers/role/removeRoleResource.ts +++ b/server/routers/role/removeRoleResource.ts @@ -9,13 +9,20 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeRoleResourceParamsSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const removeRoleResourceParamsSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); -const removeRoleResourceSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const removeRoleResourceSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export async function removeRoleResource( req: Request, @@ -71,7 +78,7 @@ export async function removeRoleResource( success: true, error: false, message: "Resource removed from role successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/removeRoleSite.ts b/server/routers/role/removeRoleSite.ts index bebbbd9..43efca5 100644 --- a/server/routers/role/removeRoleSite.ts +++ b/server/routers/role/removeRoleSite.ts @@ -9,13 +9,17 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeRoleSiteParamsSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const removeRoleSiteParamsSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); -const removeRoleSiteSchema = z.object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const removeRoleSiteSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function removeRoleSite( req: Request, @@ -85,7 +89,7 @@ export async function removeRoleSite( success: true, error: false, message: "Site removed from role successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts index 574e1c3..0ba10ab 100644 --- a/server/routers/role/updateRole.ts +++ b/server/routers/role/updateRole.ts @@ -9,18 +9,20 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const updateRoleParamsSchema = z.object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const updateRoleParamsSchema = z + .object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); const updateRoleBodySchema = z .object({ name: z.string().min(1).max(255).optional(), - description: z.string().optional(), + description: z.string().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update", + message: "At least one field must be provided for update" }); export async function updateRole( @@ -96,7 +98,7 @@ export async function updateRole( success: true, error: false, message: "Role updated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index fec4f78..99a74be 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles, userSites, sites, roleSites } from "@server/db/schema"; +import { roles, userSites, sites, roleSites, Site } from "@server/db/schema"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -14,9 +14,11 @@ import { hash } from "@node-rs/argon2"; import { newts } from "@server/db/schema"; import moment from "moment"; -const createSiteParamsSchema = z.object({ - orgId: z.string(), -}); +const createSiteParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); const createSiteSchema = z .object({ @@ -27,18 +29,13 @@ const createSiteSchema = z subnet: z.string(), newtId: z.string().optional(), secret: z.string().optional(), - type: z.string(), + type: z.string() }) .strict(); export type CreateSiteBody = z.infer; -export type CreateSiteResponse = { - name: string; - siteId: number; - orgId: string; - niceId: string; -}; +export type CreateSiteResponse = Site; export async function createSite( req: Request, @@ -85,14 +82,14 @@ export async function createSite( name, niceId, subnet, - type, + type }; if (pubKey && type == "wireguard") { // we dont add the pubKey for newts because the newt will generate it payload = { ...payload, - pubKey, + pubKey }; } @@ -112,14 +109,14 @@ export async function createSite( await db.insert(roleSites).values({ roleId: adminRole[0].roleId, - siteId: newSite.siteId, + siteId: newSite.siteId }); if (req.userOrgRoleId != adminRole[0].roleId) { // make sure the user can access the site db.insert(userSites).values({ userId: req.user?.userId!, - siteId: newSite.siteId, + siteId: newSite.siteId }); } @@ -129,14 +126,14 @@ export async function createSite( memoryCost: 19456, timeCost: 2, outputLen: 32, - parallelism: 1, + parallelism: 1 }); await db.insert(newts).values({ newtId: newtId!, secretHash, siteId: newSite.siteId, - dateCreated: moment().toISOString(), + dateCreated: moment().toISOString() }); } else if (type == "wireguard") { if (!pubKey) { @@ -149,23 +146,19 @@ export async function createSite( } await addPeer(exitNodeId, { publicKey: pubKey, - allowedIps: [], + allowedIps: [] }); } - return response(res, { - data: { - name: newSite.name, - niceId: newSite.niceId, - siteId: newSite.siteId, - orgId: newSite.orgId, - }, + return response(res, { + data: newSite, success: true, error: false, message: "Site created successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { + logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 4082d8e..8d6421f 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -11,9 +11,11 @@ import { deletePeer } from "../gerbil/peers"; import { fromError } from "zod-validation-error"; import { sendToClient } from "../ws"; -const deleteSiteSchema = z.object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const deleteSiteSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function deleteSite( req: Request, @@ -60,7 +62,7 @@ export async function deleteSite( if (deletedNewt) { const payload = { type: `newt/terminate`, - data: {}, + data: {} }; sendToClient(deletedNewt.newtId, payload); @@ -79,7 +81,7 @@ export async function deleteSite( success: true, error: false, message: "Site deleted successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index d49ff63..6c259e3 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -10,16 +10,18 @@ import logger from "@server/logger"; import stoi from "@server/utils/stoi"; import { fromError } from "zod-validation-error"; -const getSiteSchema = z.object({ - siteId: z - .string() - .optional() - .transform(stoi) - .pipe(z.number().int().positive().optional()) - .optional(), - niceId: z.string().optional(), - orgId: z.string().optional(), -}); +const getSiteSchema = z + .object({ + siteId: z + .string() + .optional() + .transform(stoi) + .pipe(z.number().int().positive().optional()) + .optional(), + niceId: z.string().optional(), + orgId: z.string().optional() + }) + .strict(); export type GetSiteResponse = { siteId: number; @@ -79,15 +81,15 @@ export async function getSite( siteId: site[0].siteId, niceId: site[0].niceId, name: site[0].name, - subnet: site[0].subnet, + subnet: site[0].subnet }, success: true, error: false, message: "Site retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { - logger.error("Error from getSite: ", error); + logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); diff --git a/server/routers/site/listSiteRoles.ts b/server/routers/site/listSiteRoles.ts index b101179..c347a08 100644 --- a/server/routers/site/listSiteRoles.ts +++ b/server/routers/site/listSiteRoles.ts @@ -9,9 +9,11 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listSiteRolesSchema = z.object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const listSiteRolesSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function listSiteRoles( req: Request, @@ -36,7 +38,7 @@ export async function listSiteRoles( roleId: roles.roleId, name: roles.name, description: roles.description, - isAdmin: roles.isAdmin, + isAdmin: roles.isAdmin }) .from(roleSites) .innerJoin(roles, eq(roleSites.roleId, roles.roleId)) @@ -47,7 +49,7 @@ export async function listSiteRoles( success: true, error: false, message: "Site roles retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index a4f885e..4046944 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -1,5 +1,6 @@ import { db } from "@server/db"; import { orgs, roleSites, sites, userSites } from "@server/db/schema"; +import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/utils/response"; import { and, count, eq, inArray, or, sql } from "drizzle-orm"; @@ -8,9 +9,11 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -const listSitesParamsSchema = z.object({ - orgId: z.string(), -}); +const listSitesParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); const listSitesSchema = z.object({ limit: z @@ -24,7 +27,7 @@ const listSitesSchema = z.object({ .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.number().int().nonnegative()) }); function querySites(orgId: string, accessibleSiteIds: number[]) { @@ -39,15 +42,15 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { megabytesOut: sites.megabytesOut, orgName: orgs.name, type: sites.type, - online: sites.online, + online: sites.online }) .from(sites) .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .where( and( inArray(sites.siteId, accessibleSiteIds), - eq(sites.orgId, orgId), - ), + eq(sites.orgId, orgId) + ) ); } @@ -59,7 +62,7 @@ export type ListSitesResponse = { export async function listSites( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { try { const parsedQuery = listSitesSchema.safeParse(req.query); @@ -67,8 +70,8 @@ export async function listSites( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedQuery.error), - ), + fromError(parsedQuery.error) + ) ); } const { limit, offset } = parsedQuery.data; @@ -78,8 +81,8 @@ export async function listSites( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error), - ), + fromError(parsedParams.error) + ) ); } const { orgId } = parsedParams.data; @@ -88,22 +91,22 @@ export async function listSites( return next( createHttpError( HttpCode.FORBIDDEN, - "User does not have access to this organization", - ), + "User does not have access to this organization" + ) ); } const accessibleSites = await db .select({ - siteId: sql`COALESCE(${userSites.siteId}, ${roleSites.siteId})`, + siteId: sql`COALESCE(${userSites.siteId}, ${roleSites.siteId})` }) .from(userSites) .fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId)) .where( or( eq(userSites.userId, req.user!.userId), - eq(roleSites.roleId, req.userOrgRoleId!), - ), + eq(roleSites.roleId, req.userOrgRoleId!) + ) ); const accessibleSiteIds = accessibleSites.map((site) => site.siteId); @@ -115,8 +118,8 @@ export async function listSites( .where( and( inArray(sites.siteId, accessibleSiteIds), - eq(sites.orgId, orgId), - ), + eq(sites.orgId, orgId) + ) ); const sitesList = await baseQuery.limit(limit).offset(offset); @@ -129,20 +132,18 @@ export async function listSites( pagination: { total: totalCount, limit, - offset, - }, + offset + } }, success: true, error: false, message: "Sites retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { + logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred", - ), + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index 70c60fd..41e764a 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -9,14 +9,16 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const updateSiteParamsSchema = z.object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const updateSiteParamsSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); const updateSiteBodySchema = z .object({ name: z.string().min(1).max(255).optional(), - subdomain: z.string().min(1).max(255).optional(), + subdomain: z.string().min(1).max(255).optional() // pubKey: z.string().optional(), // subnet: z.string().optional(), // exitNode: z.number().int().positive().optional(), @@ -25,7 +27,7 @@ const updateSiteBodySchema = z }) .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update", + message: "At least one field must be provided for update" }); export async function updateSite( @@ -77,7 +79,7 @@ export async function updateSite( success: true, error: false, message: "Site updated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 11b73dc..caa8448 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -12,9 +12,14 @@ import { isIpInCidr } from "@server/utils/ip"; import { fromError } from "zod-validation-error"; import { addTargets } from "../newt/targets"; -const createTargetParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const createTargetParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); const createTargetSchema = z .object({ @@ -22,7 +27,7 @@ const createTargetSchema = z method: z.string().min(1).max(10), port: z.number().int().min(1).max(65535), protocol: z.string().optional(), - enabled: z.boolean().default(true), + enabled: z.boolean().default(true) }) .strict(); @@ -61,7 +66,7 @@ export async function createTarget( // get the resource const [resource] = await db .select({ - siteId: resources.siteId, + siteId: resources.siteId }) .from(resources) .where(eq(resources.resourceId, resourceId)); @@ -91,7 +96,10 @@ export async function createTarget( } // make sure the target is within the site subnet - if (site.type == "wireguard" && !isIpInCidr(targetData.ip, site.subnet!)) { + if ( + site.type == "wireguard" && + !isIpInCidr(targetData.ip, site.subnet!) + ) { return next( createHttpError( HttpCode.BAD_REQUEST, @@ -102,7 +110,7 @@ export async function createTarget( // Fetch resources for this site const resourcesRes = await db.query.resources.findMany({ - where: eq(resources.siteId, site.siteId), + where: eq(resources.siteId, site.siteId) }); // TODO: is this all inefficient? @@ -112,7 +120,7 @@ export async function createTarget( await Promise.all( resourcesRes.map(async (resource) => { const targetsRes = await db.query.targets.findMany({ - where: eq(targets.resourceId, resource.resourceId), + where: eq(targets.resourceId, resource.resourceId) }); targetsRes.forEach((target) => { targetIps.push(`${target.ip}/32`); @@ -147,7 +155,7 @@ export async function createTarget( resourceId, protocol: "tcp", // hard code for now internalPort, - ...targetData, + ...targetData }) .returning(); @@ -155,7 +163,7 @@ export async function createTarget( if (site.type == "wireguard") { await addPeer(site.exitNodeId!, { publicKey: site.pubKey, - allowedIps: targetIps.flat(), + allowedIps: targetIps.flat() }); } else if (site.type == "newt") { // get the newt on the site by querying the newt table for siteId @@ -174,7 +182,7 @@ export async function createTarget( success: true, error: false, message: "Target created successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index eb89bfa..1d9d337 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -11,9 +11,11 @@ import { addPeer } from "../gerbil/peers"; import { fromError } from "zod-validation-error"; import { removeTargets } from "../newt/targets"; -const deleteTargetSchema = z.object({ - targetId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const deleteTargetSchema = z + .object({ + targetId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function deleteTarget( req: Request, @@ -49,7 +51,7 @@ export async function deleteTarget( // get the resource const [resource] = await db .select({ - siteId: resources.siteId, + siteId: resources.siteId }) .from(resources) .where(eq(resources.resourceId, deletedTarget.resourceId!)); @@ -83,14 +85,14 @@ export async function deleteTarget( // TODO: is this all inefficient? // Fetch resources for this site const resourcesRes = await db.query.resources.findMany({ - where: eq(resources.siteId, site.siteId), + where: eq(resources.siteId, site.siteId) }); // Fetch targets for all resources of this site const targetIps = await Promise.all( resourcesRes.map(async (resource) => { const targetsRes = await db.query.targets.findMany({ - where: eq(targets.resourceId, resource.resourceId), + where: eq(targets.resourceId, resource.resourceId) }); return targetsRes.map((target) => `${target.ip}/32`); }) @@ -98,7 +100,7 @@ export async function deleteTarget( await addPeer(site.exitNodeId!, { publicKey: site.pubKey, - allowedIps: targetIps.flat(), + allowedIps: targetIps.flat() }); } else if (site.type == "newt") { // get the newt on the site by querying the newt table for siteId @@ -117,7 +119,7 @@ export async function deleteTarget( success: true, error: false, message: "Target deleted successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 745892a..21ee78c 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -9,9 +9,11 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const getTargetSchema = z.object({ - targetId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const getTargetSchema = z + .object({ + targetId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function getTarget( req: Request, @@ -51,7 +53,7 @@ export async function getTarget( success: true, error: false, message: "Target retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index b4d6317..b39fb0a 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -9,9 +9,14 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; -const listTargetsParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const listTargetsParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); const listTargetsSchema = z.object({ limit: z @@ -25,7 +30,7 @@ const listTargetsSchema = z.object({ .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.number().int().nonnegative()) }); function queryTargets(resourceId: number) { @@ -37,7 +42,7 @@ function queryTargets(resourceId: number) { port: targets.port, protocol: targets.protocol, enabled: targets.enabled, - resourceId: targets.resourceId, + resourceId: targets.resourceId // resourceName: resources.name, }) .from(targets) @@ -97,13 +102,13 @@ export async function listTargets( pagination: { total: totalCount, limit, - offset, - }, + offset + } }, success: true, error: false, message: "Targets retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index b1b453c..a1f8620 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -11,20 +11,22 @@ import { fromError } from "zod-validation-error"; import { addPeer } from "../gerbil/peers"; import { addTargets } from "../newt/targets"; -const updateTargetParamsSchema = z.object({ - targetId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const updateTargetParamsSchema = z + .object({ + targetId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); const updateTargetBodySchema = z .object({ ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete method: z.string().min(1).max(10).optional(), port: z.number().int().min(1).max(65535).optional(), - enabled: z.boolean().optional(), + enabled: z.boolean().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update", + message: "At least one field must be provided for update" }); export async function updateTarget( @@ -74,7 +76,7 @@ export async function updateTarget( // get the resource const [resource] = await db .select({ - siteId: resources.siteId, + siteId: resources.siteId }) .from(resources) .where(eq(resources.resourceId, updatedTarget.resourceId!)); @@ -107,14 +109,14 @@ export async function updateTarget( // TODO: is this all inefficient? // Fetch resources for this site const resourcesRes = await db.query.resources.findMany({ - where: eq(resources.siteId, site.siteId), + where: eq(resources.siteId, site.siteId) }); // Fetch targets for all resources of this site const targetIps = await Promise.all( resourcesRes.map(async (resource) => { const targetsRes = await db.query.targets.findMany({ - where: eq(targets.resourceId, resource.resourceId), + where: eq(targets.resourceId, resource.resourceId) }); return targetsRes.map((target) => `${target.ip}/32`); }) @@ -122,7 +124,7 @@ export async function updateTarget( await addPeer(site.exitNodeId!, { publicKey: site.pubKey, - allowedIps: targetIps.flat(), + allowedIps: targetIps.flat() }); } else if (site.type == "newt") { // get the newt on the site by querying the newt table for siteId @@ -140,7 +142,7 @@ export async function updateTarget( success: true, error: false, message: "Target updated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts index 34af968..172fa46 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -11,10 +11,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { isWithinExpirationDate } from "oslo"; -const acceptInviteBodySchema = z.object({ - token: z.string(), - inviteId: z.string(), -}); +const acceptInviteBodySchema = z + .object({ + token: z.string(), + inviteId: z.string() + }) + .strict(); export type AcceptInviteResponse = { accepted: boolean; @@ -64,7 +66,7 @@ export async function acceptInvite( memoryCost: 19456, timeCost: 2, outputLen: 32, - parallelism: 1, + parallelism: 1 }); if (!validToken) { return next( @@ -121,7 +123,7 @@ export async function acceptInvite( await db.insert(userOrgs).values({ userId: existingUser[0].userId, orgId: existingInvite[0].orgId, - roleId: existingInvite[0].roleId, + roleId: existingInvite[0].roleId }); // delete the invite @@ -132,7 +134,7 @@ export async function acceptInvite( success: true, error: false, message: "Invite accepted", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/addUserAction.ts b/server/routers/user/addUserAction.ts index 4e63835..0352982 100644 --- a/server/routers/user/addUserAction.ts +++ b/server/routers/user/addUserAction.ts @@ -9,11 +9,13 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addUserActionSchema = z.object({ - userId: z.string(), - actionId: z.string(), - orgId: z.string(), -}); +const addUserActionSchema = z + .object({ + userId: z.string(), + actionId: z.string(), + orgId: z.string() + }) + .strict(); export async function addUserAction( req: Request, @@ -52,7 +54,7 @@ export async function addUserAction( .values({ userId, actionId, - orgId, + orgId }) .returning(); @@ -61,7 +63,7 @@ export async function addUserAction( success: true, error: false, message: "Action added to user successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/server/routers/user/addUserRole.ts b/server/routers/user/addUserRole.ts index 81c7b07..0b3acf6 100644 --- a/server/routers/user/addUserRole.ts +++ b/server/routers/user/addUserRole.ts @@ -10,10 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import stoi from "@server/utils/stoi"; -const addUserRoleParamsSchema = z.object({ - userId: z.string(), - roleId: z.string().transform(stoi).pipe(z.number()), -}); +const addUserRoleParamsSchema = z + .object({ + userId: z.string(), + roleId: z.string().transform(stoi).pipe(z.number()) + }) + .strict(); export type AddUserRoleResponse = z.infer; @@ -96,7 +98,7 @@ export async function addUserRole( success: true, error: false, message: "Role added to user successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts index 27d90e9..22a08e0 100644 --- a/server/routers/user/addUserSite.ts +++ b/server/routers/user/addUserSite.ts @@ -9,10 +9,12 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addUserSiteSchema = z.object({ - userId: z.string(), - siteId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const addUserSiteSchema = z + .object({ + userId: z.string(), + siteId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); export async function addUserSite( req: Request, @@ -36,7 +38,7 @@ export async function addUserSite( .insert(userSites) .values({ userId, - siteId, + siteId }) .returning(); @@ -48,7 +50,7 @@ export async function addUserSite( for (const resource of siteResources) { await db.insert(userResources).values({ userId, - resourceId: resource.resourceId, + resourceId: resource.resourceId }); } @@ -57,7 +59,7 @@ export async function addUserSite( success: true, error: false, message: "Site added to user successfully", - status: HttpCode.CREATED, + status: HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 4f67546..3f4a7b6 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -19,7 +19,7 @@ async function queryUser(orgId: string, userId: string) { roleId: userOrgs.roleId, roleName: roles.name, isOwner: userOrgs.isOwner, - isAdmin: roles.isAdmin, + isAdmin: roles.isAdmin }) .from(userOrgs) .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) @@ -33,10 +33,12 @@ export type GetOrgUserResponse = NonNullable< Awaited> >; -const getOrgUserParamsSchema = z.object({ - userId: z.string(), - orgId: z.string(), -}); +const getOrgUserParamsSchema = z + .object({ + userId: z.string(), + orgId: z.string() + }) + .strict(); export async function getOrgUser( req: Request, @@ -109,7 +111,7 @@ export async function getOrgUser( success: true, error: false, message: "User retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 84c6eb4..318a08b 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -6,7 +6,6 @@ import { and, eq } from "drizzle-orm"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import logger from "@server/logger"; import { alphabet, generateRandomString } from "oslo/crypto"; import { createDate, TimeSpan } from "oslo"; @@ -16,15 +15,20 @@ import { fromError } from "zod-validation-error"; import { sendEmail } from "@server/emails"; import SendInviteLink from "@server/emails/templates/SendInviteLink"; -const inviteUserParamsSchema = z.object({ - orgId: z.string(), -}); +const inviteUserParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); -const inviteUserBodySchema = z.object({ - email: z.string().email(), - roleId: z.number(), - validHours: z.number().gt(0).lte(168), -}); +const inviteUserBodySchema = z + .object({ + email: z.string().email(), + roleId: z.number(), + validHours: z.number().gt(0).lte(168), + sendEmail: z.boolean().optional() + }) + .strict(); export type InviteUserBody = z.infer; @@ -38,7 +42,7 @@ const inviteTracker: Record = {}; export async function inviteUser( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { try { const parsedParams = inviteUserParamsSchema.safeParse(req.params); @@ -46,8 +50,8 @@ export async function inviteUser( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString(), - ), + fromError(parsedParams.error).toString() + ) ); } @@ -56,13 +60,18 @@ export async function inviteUser( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString(), - ), + fromError(parsedBody.error).toString() + ) ); } const { orgId } = parsedParams.data; - const { email, validHours, roleId } = parsedBody.data; + const { + email, + validHours, + roleId, + sendEmail: doEmail + } = parsedBody.data; const currentTime = Date.now(); const oneHourAgo = currentTime - 3600000; @@ -79,8 +88,8 @@ export async function inviteUser( return next( createHttpError( HttpCode.TOO_MANY_REQUESTS, - "User has already been invited 3 times in the last hour", - ), + "User has already been invited 3 times in the last hour" + ) ); } @@ -93,7 +102,7 @@ export async function inviteUser( .limit(1); if (!org.length) { return next( - createHttpError(HttpCode.NOT_FOUND, "Organization not found"), + createHttpError(HttpCode.NOT_FOUND, "Organization not found") ); } @@ -107,14 +116,14 @@ export async function inviteUser( return next( createHttpError( HttpCode.BAD_REQUEST, - "User is already a member of this organization", - ), + "User is already a member of this organization" + ) ); } const inviteId = generateRandomString( 10, - alphabet("a-z", "A-Z", "0-9"), + alphabet("a-z", "A-Z", "0-9") ); const token = generateRandomString(32, alphabet("a-z", "A-Z", "0-9")); const expiresAt = createDate(new TimeSpan(validHours, "h")).getTime(); @@ -125,7 +134,7 @@ export async function inviteUser( await db .delete(userInvites) .where( - and(eq(userInvites.email, email), eq(userInvites.orgId, orgId)), + and(eq(userInvites.email, email), eq(userInvites.orgId, orgId)) ) .execute(); @@ -135,43 +144,42 @@ export async function inviteUser( email, expiresAt, tokenHash, - roleId, + roleId }); const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`; - await sendEmail( - SendInviteLink({ - email, - inviteLink, - expiresInDays: (validHours / 24).toString(), - orgName: org[0].name || orgId, - inviterName: req.user?.email, - }), - { - to: email, - from: config.email?.no_reply, - subject: "You're invited to join a Fossorial organization", - }, - ); + if (doEmail) { + await sendEmail( + SendInviteLink({ + email, + inviteLink, + expiresInDays: (validHours / 24).toString(), + orgName: org[0].name || orgId, + inviterName: req.user?.email + }), + { + to: email, + from: config.email?.no_reply, + subject: "You're invited to join a Fossorial organization" + } + ); + } return response(res, { data: { inviteLink, - expiresAt, + expiresAt }, success: true, error: false, message: "User invited successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { - console.error(error); + logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred", - ), + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index b40f019..7202735 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -8,24 +8,28 @@ import createHttpError from "http-errors"; import { sql } from "drizzle-orm"; import logger from "@server/logger"; -const listUsersParamsSchema = z.object({ - orgId: z.string(), -}); +const listUsersParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); -const listUsersSchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()), -}); +const listUsersSchema = z + .object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) + }) + .strict(); async function queryUsers(orgId: string, limit: number, offset: number) { return await db @@ -37,7 +41,7 @@ async function queryUsers(orgId: string, limit: number, offset: number) { orgId: userOrgs.orgId, roleId: userOrgs.roleId, roleName: roles.name, - isOwner: userOrgs.isOwner, + isOwner: userOrgs.isOwner }) .from(users) .leftJoin(userOrgs, sql`${users.userId} = ${userOrgs.userId}`) @@ -97,13 +101,13 @@ export async function listUsers( pagination: { total: count, limit, - offset, - }, + offset + } }, success: true, error: false, message: "Users retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/removeUserAction.ts b/server/routers/user/removeUserAction.ts index 838e099..0ccb260 100644 --- a/server/routers/user/removeUserAction.ts +++ b/server/routers/user/removeUserAction.ts @@ -9,14 +9,18 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserActionParamsSchema = z.object({ - userId: z.string(), -}); +const removeUserActionParamsSchema = z + .object({ + userId: z.string() + }) + .strict(); -const removeUserActionSchema = z.object({ - actionId: z.string(), - orgId: z.string(), -}); +const removeUserActionSchema = z + .object({ + actionId: z.string(), + orgId: z.string() + }) + .strict(); export async function removeUserAction( req: Request, @@ -73,7 +77,7 @@ export async function removeUserAction( success: true, error: false, message: "Action removed from user successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index c3072bc..5c5d305 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -9,10 +9,12 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserSchema = z.object({ - userId: z.string(), - orgId: z.string(), -}); +const removeUserSchema = z + .object({ + userId: z.string(), + orgId: z.string() + }) + .strict(); export async function removeUserOrg( req: Request, @@ -70,7 +72,7 @@ export async function removeUserOrg( success: true, error: false, message: "User remove from org successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/removeUserResource.ts b/server/routers/user/removeUserResource.ts index d62fcf9..318b6b3 100644 --- a/server/routers/user/removeUserResource.ts +++ b/server/routers/user/removeUserResource.ts @@ -9,10 +9,15 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserResourceSchema = z.object({ - userId: z.string(), - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}); +const removeUserResourceSchema = z + .object({ + userId: z.string(), + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); export async function removeUserResource( req: Request, @@ -56,7 +61,7 @@ export async function removeUserResource( success: true, error: false, message: "Resource removed from user successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/user/removeUserSite.ts b/server/routers/user/removeUserSite.ts index 748f54c..153f76b 100644 --- a/server/routers/user/removeUserSite.ts +++ b/server/routers/user/removeUserSite.ts @@ -9,13 +9,17 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserSiteParamsSchema = z.object({ - userId: z.string(), -}); +const removeUserSiteParamsSchema = z + .object({ + userId: z.string() + }) + .strict(); -const removeUserSiteSchema = z.object({ - siteId: z.number().int().positive(), -}); +const removeUserSiteSchema = z + .object({ + siteId: z.number().int().positive() + }) + .strict(); export async function removeUserSite( req: Request, @@ -85,7 +89,7 @@ export async function removeUserSite( success: true, error: false, message: "Site removed from user successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx b/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx index dca58ae..76b2491 100644 --- a/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx +++ b/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx @@ -126,7 +126,7 @@ export default function CreateRoleForm({
); - }, + } }, { accessorKey: "description", - header: "Description", + header: "Description" }, { id: "actions", @@ -90,16 +90,15 @@ export default function UsersTable({ roles: r }: RolesTableProps) { - - + @@ -107,8 +106,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) { ); - }, - }, + } + } ]; return ( @@ -128,9 +127,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) { roleToDelete={roleToRemove} afterDelete={() => { setRoles((prev) => - prev.filter( - (r) => r.roleId !== roleToRemove.roleId, - ), + prev.filter((r) => r.roleId !== roleToRemove.roleId) ); setUserToRemove(null); }} diff --git a/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx b/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx index 0a18c97..daf44d9 100644 --- a/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx +++ b/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx @@ -7,7 +7,7 @@ import { FormField, FormItem, FormLabel, - FormMessage, + FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { @@ -15,7 +15,7 @@ import { SelectContent, SelectItem, SelectTrigger, - SelectValue, + SelectValue } from "@app/components/ui/select"; import { useToast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -33,13 +33,14 @@ import { CredenzaDescription, CredenzaFooter, CredenzaHeader, - CredenzaTitle, + CredenzaTitle } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { ListRolesResponse } from "@server/routers/role"; import { formatAxiosError } from "@app/lib/utils"; import { createApiClient } from "@app/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Checkbox } from "@app/components/ui/checkbox"; type InviteUserFormProps = { open: boolean; @@ -49,14 +50,16 @@ type InviteUserFormProps = { const formSchema = z.object({ email: z.string().email({ message: "Invalid email address" }), validForHours: z.string().min(1, { message: "Please select a duration" }), - roleId: z.string().min(1, { message: "Please select a role" }), + roleId: z.string().min(1, { message: "Please select a role" }) }); export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { const { toast } = useToast(); const { org } = useOrgContext(); - const api = createApiClient(useEnvContext()); + const { env } = useEnvContext(); + + const api = createApiClient({ env }); const [inviteLink, setInviteLink] = useState(null); const [loading, setLoading] = useState(false); @@ -64,6 +67,8 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); + const [sendEmail, setSendEmail] = useState(env.EMAIL_ENABLED === "true"); + const validFor = [ { hours: 24, name: "1 day" }, { hours: 48, name: "2 days" }, @@ -71,7 +76,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { { hours: 96, name: "4 days" }, { hours: 120, name: "5 days" }, { hours: 144, name: "6 days" }, - { hours: 168, name: "7 days" }, + { hours: 168, name: "7 days" } ]; const form = useForm>({ @@ -79,8 +84,8 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { defaultValues: { email: "", validForHours: "72", - roleId: "", - }, + roleId: "" + } }); useEffect(() => { @@ -90,9 +95,9 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { async function fetchRoles() { const res = await api - .get>( - `/org/${org?.org.orgId}/roles` - ) + .get< + AxiosResponse + >(`/org/${org?.org.orgId}/roles`) .catch((e) => { console.error(e); toast({ @@ -101,7 +106,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { description: formatAxiosError( e, "An error occurred while fetching the roles" - ), + ) }); }); @@ -127,6 +132,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { email: values.email, roleId: parseInt(values.roleId), validHours: parseInt(values.validForHours), + sendEmail: sendEmail } as InviteUserBody ) .catch((e) => { @@ -136,7 +142,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { description: formatAxiosError( e, "An error occurred while inviting the user" - ), + ) }); }); @@ -145,7 +151,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { toast({ variant: "default", title: "User invited", - description: "The user has been successfully invited.", + description: "The user has been successfully invited." }); setExpiresInDays(parseInt(values.validForHours) / 24); @@ -198,6 +204,27 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { )} /> + + {env.EMAIL_ENABLED === "true" && ( +
+ + setSendEmail( + e as boolean + ) + } + /> + +
+ )} + -

- The user has been successfully invited. - They must access the link below to - accept the invitation. -

+ {sendEmail && ( +

+ An email has been sent to the user + with the access link below. They + must access the link to accept the + invitation. +

+ )} + {!sendEmail && ( +

+ The user has been invited. They must + access the link below to accept the + invitation. +

+ )}

The invite will expire in{" "} diff --git a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx index 23c3eda..7b71932 100644 --- a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx @@ -5,7 +5,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; @@ -43,7 +43,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { const router = useRouter(); - const api = createApiClient(useEnvContext()); + const api = createApiClient(useEnvContext()); const user = useUserContext(); const { org } = useOrgContext(); @@ -64,7 +64,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { ); - }, + } }, { accessorKey: "status", @@ -80,7 +80,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { ); - }, + } }, { accessorKey: "role", @@ -108,7 +108,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { {userRow.role} ); - }, + } }, { id: "actions", @@ -149,20 +149,19 @@ export default function UsersTable({ users: u }: UsersTableProps) { {userRow.email !== user?.email && ( - - + )} @@ -183,8 +182,8 @@ export default function UsersTable({ users: u }: UsersTableProps) { ); - }, - }, + } + } ]; async function removeUser() { @@ -197,8 +196,8 @@ export default function UsersTable({ users: u }: UsersTableProps) { title: "Failed to remove user", description: formatAxiosError( e, - "An error occurred while removing the user.", - ), + "An error occurred while removing the user." + ) }); }); @@ -206,11 +205,11 @@ export default function UsersTable({ users: u }: UsersTableProps) { toast({ variant: "default", title: "User removed", - description: `The user ${selectedUser.email} has been removed from the organization.`, + description: `The user ${selectedUser.email} has been removed from the organization.` }); setUsers((prev) => - prev.filter((u) => u.id !== selectedUser?.id), + prev.filter((u) => u.id !== selectedUser?.id) ); } } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index aae68d1..c9821a7 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -637,63 +637,72 @@ export default function ResourceAuthenticationPage() { - - - ( - - - Whitelisted Emails - - - { - return z - .string() - .email() - .safeParse(tag) - .success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder="Enter an email" - tags={ - whitelistForm.getValues() - .emails - } - setTags={(newRoles) => { - whitelistForm.setValue( - "emails", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - allowDuplicates={false} - sortTags={true} - styleClasses={{ - tag: { - body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full" - }, - input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none", - inlineTagsContainer: - "bg-transparent" - }} - /> - - - )} - /> - - + {whitelistEnabled && ( +

+ + ( + + + Whitelisted Emails + + + { + return z + .string() + .email() + .safeParse( + tag + ).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder="Enter an email" + tags={ + whitelistForm.getValues() + .emails + } + setTags={( + newRoles + ) => { + whitelistForm.setValue( + "emails", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={ + false + } + sortTags={true} + styleClasses={{ + tag: { + body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full" + }, + input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none", + inlineTagsContainer: + "bg-transparent" + }} + /> + + + )} + /> + + + )} ); - }, + } }, { accessorKey: "site", @@ -117,7 +117,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { ); - }, + } }, { accessorKey: "domain", @@ -139,16 +139,16 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { className="h-6 w-6 p-0" onClick={() => { navigator.clipboard.writeText( - resourceRow.domain, + resourceRow.domain ); const originalIcon = document.querySelector( - `#icon-${resourceRow.id}`, + `#icon-${resourceRow.id}` ); if (originalIcon) { originalIcon.classList.add("hidden"); } const checkIcon = document.querySelector( - `#check-icon-${resourceRow.id}`, + `#check-icon-${resourceRow.id}` ); if (checkIcon) { checkIcon.classList.remove("hidden"); @@ -156,7 +156,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { checkIcon.classList.add("hidden"); if (originalIcon) { originalIcon.classList.remove( - "hidden", + "hidden" ); } }, 2000); @@ -175,7 +175,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { ); - }, + } }, { accessorKey: "hasAuth", @@ -209,7 +209,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { )} ); - }, + } }, { id: "actions", @@ -241,18 +241,15 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { View settings - - + @@ -267,8 +264,8 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { ); - }, - }, + } + } ]; return ( diff --git a/src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx b/src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx index c41f368..b94e14f 100644 --- a/src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx +++ b/src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx @@ -197,12 +197,16 @@ export default function CreateShareLinkForm({ const link = constructShareLink( values.resourceId, token.accessTokenId, - token.tokenHash + token.accessToken ); setLink(link); onCreated?.({ - ...token, - resourceName: values.resourceName + accessTokenId: token.accessTokenId, + resourceId: token.resourceId, + resourceName: values.resourceName, + title: token.title, + createdAt: token.createdAt, + expiresAt: token.expiresAt }); } @@ -285,7 +289,9 @@ export default function CreateShareLinkForm({ r ) => ( +

+ You will be able to see this link once. + Make sure to copy it. +

Anyone with this link can access the resource. Share it with care. diff --git a/src/app/[orgId]/settings/share-links/components/ShareLinksTable.tsx b/src/app/[orgId]/settings/share-links/components/ShareLinksTable.tsx index f6c3bea..958cfd0 100644 --- a/src/app/[orgId]/settings/share-links/components/ShareLinksTable.tsx +++ b/src/app/[orgId]/settings/share-links/components/ShareLinksTable.tsx @@ -34,9 +34,14 @@ import moment from "moment"; import CreateShareLinkForm from "./CreateShareLinkForm"; import { constructShareLink } from "@app/lib/shareLinks"; -export type ShareLinkRow = ArrayElement< - ListAccessTokensResponse["accessTokens"] ->; +export type ShareLinkRow = { + accessTokenId: string; + resourceId: number; + resourceName: string; + title: string | null; + createdAt: number; + expiresAt: number | null; +}; type ShareLinksTableProps = { shareLinks: ShareLinkRow[]; @@ -64,7 +69,10 @@ export default function ShareLinksTable({ await api.delete(`/access-token/${id}`).catch((e) => { toast({ title: "Failed to delete link", - description: formatAxiosError(e, "An error occurred deleting link"), + description: formatAxiosError( + e, + "An error occurred deleting link" + ) }); }); @@ -73,7 +81,7 @@ export default function ShareLinksTable({ toast({ title: "Link deleted", - description: "The link has been deleted", + description: "The link has been deleted" }); } @@ -123,69 +131,69 @@ export default function ShareLinksTable({ ); } }, - { - accessorKey: "domain", - header: "Link", - cell: ({ row }) => { - const r = row.original; - - const link = constructShareLink( - r.resourceId, - r.accessTokenId, - r.tokenHash - ); - - return ( -

- - {formatLink(link)} - - -
- ); - } - }, + // { + // accessorKey: "domain", + // header: "Link", + // cell: ({ row }) => { + // const r = row.original; + // + // const link = constructShareLink( + // r.resourceId, + // r.accessTokenId, + // r.tokenHash + // ); + // + // return ( + //
+ // + // {formatLink(link)} + // + // + //
+ // ); + // } + // }, { accessorKey: "createdAt", header: ({ column }) => { diff --git a/src/app/[orgId]/settings/share-links/page.tsx b/src/app/[orgId]/settings/share-links/page.tsx index 558ec27..21c562a 100644 --- a/src/app/[orgId]/settings/share-links/page.tsx +++ b/src/app/[orgId]/settings/share-links/page.tsx @@ -46,9 +46,9 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) { redirect(`/${params.orgId}/settings/resources`); } - const rows: ShareLinkRow[] = tokens.map((token) => { - return token; - }); + const rows: ShareLinkRow[] = tokens.map( + (token) => ({ ...token }) as ShareLinkRow + ); return ( <> diff --git a/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx index eaf6462..b26c6f4 100644 --- a/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx +++ b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx @@ -8,7 +8,7 @@ import { FormField, FormItem, FormLabel, - FormMessage, + FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { useToast } from "@app/hooks/useToast"; @@ -24,11 +24,15 @@ import { CredenzaDescription, CredenzaFooter, CredenzaHeader, - CredenzaTitle, + CredenzaTitle } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { useParams, useRouter } from "next/navigation"; -import { CreateSiteBody, PickSiteDefaultsResponse } from "@server/routers/site"; +import { + CreateSiteBody, + CreateSiteResponse, + PickSiteDefaultsResponse +} from "@server/routers/site"; import { generateKeypair } from "../[niceId]/components/wireguardConfig"; import CopyTextBox from "@app/components/CopyTextBox"; import { Checkbox } from "@app/components/ui/checkbox"; @@ -37,42 +41,49 @@ import { SelectContent, SelectItem, SelectTrigger, - SelectValue, + SelectValue } from "@app/components/ui/select"; import { formatAxiosError } from "@app/lib/utils"; import { createApiClient } from "@app/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { SiteRow } from "./SitesTable"; +import { AxiosResponse } from "axios"; const method = [ { label: "Newt", value: "newt" }, - { label: "Wireguard", value: "wireguard" }, + { label: "WireGuard", value: "wireguard" } ] as const; -const accountFormSchema = z.object({ +const createSiteFormSchema = z.object({ name: z .string() .min(2, { - message: "Name must be at least 2 characters.", + message: "Name must be at least 2 characters." }) .max(30, { - message: "Name must not be longer than 30 characters.", + message: "Name must not be longer than 30 characters." }), - method: z.enum(["wireguard", "newt"]), + method: z.enum(["wireguard", "newt"]) }); -type AccountFormValues = z.infer; +type CreateSiteFormValues = z.infer; -const defaultValues: Partial = { +const defaultValues: Partial = { name: "", - method: "newt", + method: "newt" }; type CreateSiteFormProps = { open: boolean; setOpen: (open: boolean) => void; + onCreate?: (site: SiteRow) => void; }; -export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { +export default function CreateSiteForm({ + open, + setOpen, + onCreate +}: CreateSiteFormProps) { const { toast } = useToast(); const api = createApiClient(useEnvContext()); @@ -96,9 +107,9 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { setIsChecked(checked); }; - const form = useForm({ - resolver: zodResolver(accountFormSchema), - defaultValues, + const form = useForm({ + resolver: zodResolver(createSiteFormSchema), + defaultValues }); useEffect(() => { @@ -114,7 +125,7 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { toast({ variant: "destructive", title: "Error picking site defaults", - description: formatAxiosError(e), + description: formatAxiosError(e) }); }) .then((res) => { @@ -125,7 +136,7 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { } }, [open]); - async function onSubmit(data: AccountFormValues) { + async function onSubmit(data: CreateSiteFormValues) { setLoading(true); if (!siteDefaults || !keypair) { return; @@ -135,29 +146,44 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, pubKey: keypair.publicKey, - type: data.method, + type: data.method }; if (data.method === "newt") { payload.secret = siteDefaults.newtSecret; payload.newtId = siteDefaults.newtId; } const res = await api - .put(`/org/${orgId}/site/`, payload) + .put< + AxiosResponse + >(`/org/${orgId}/site/`, payload) .catch((e) => { toast({ variant: "destructive", title: "Error creating site", - description: formatAxiosError(e), + description: formatAxiosError(e) }); }); if (res && res.status === 201) { const niceId = res.data.data.niceId; // navigate to the site page - router.push(`/${orgId}/settings/sites/${niceId}`); + // router.push(`/${orgId}/settings/sites/${niceId}`); // close the modal setOpen(false); + + const data = res.data.data; + + onCreate?.({ + name: data.name, + id: data.siteId, + nice: data.niceId.toString(), + mbIn: "0 MB", + mbOut: "0 MB", + orgId: orgId as string, + type: data.type as any, + online: false + }); } setLoading(false); @@ -275,8 +301,8 @@ PersistentKeepalive = 5` {form.watch("method") === "wireguard" && !isLoading ? ( - ) : form.watch("method") === "wireguard" && - isLoading ? ( + ) : form.watch("method") === + "wireguard" && isLoading ? (

Loading WireGuard configuration... diff --git a/src/app/[orgId]/settings/sites/components/SitesTable.tsx b/src/app/[orgId]/settings/sites/components/SitesTable.tsx index 86d389f..335b54c 100644 --- a/src/app/[orgId]/settings/sites/components/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesTable.tsx @@ -6,10 +6,16 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, Check, MoreHorizontal, X } from "lucide-react"; +import { + ArrowRight, + ArrowUpDown, + Check, + MoreHorizontal, + X +} from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { AxiosResponse } from "axios"; @@ -45,14 +51,10 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedSite, setSelectedSite] = useState(null); + const [rows, setRows] = useState(sites); const api = createApiClient(useEnvContext()); - const callApi = async () => { - const res = await api.put>(`/newt`); - console.log(res); - }; - const deleteSite = (siteId: number) => { api.delete(`/site/${siteId}`) .catch((e) => { @@ -60,7 +62,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { toast({ variant: "destructive", title: "Error deleting site", - description: formatAxiosError(e, "Error deleting site"), + description: formatAxiosError(e, "Error deleting site") }); }) .then(() => { @@ -84,7 +86,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); - }, + } }, { accessorKey: "nice", @@ -100,7 +102,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); - }, + } }, { accessorKey: "mbIn", @@ -116,7 +118,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); - }, + } }, { accessorKey: "mbOut", @@ -132,7 +134,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); - }, + } }, { accessorKey: "type", @@ -167,7 +169,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); } - }, + } }, { accessorKey: "online", @@ -187,23 +189,23 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { cell: ({ row }) => { const originalRow = row.original; console.log(originalRow.online); - + if (originalRow.online) { return ( - - Online + + Online ); } else { return ( - - Offline + + Offline ); } - }, + } }, { id: "actions", @@ -229,16 +231,13 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { View settings - - + { + setSelectedSite(siteRow); + setIsDeleteModalOpen(true); + }} + > + Delete @@ -252,8 +251,8 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); - }, - }, + } + } ]; return ( @@ -261,6 +260,9 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { { + setRows([val, ...rows]); + }} /> {selectedSite && ( @@ -302,12 +304,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { { setIsCreateModalOpen(true); }} /> - {/* */} ); } diff --git a/src/app/auth/resource/[resourceId]/components/AccessToken.tsx b/src/app/auth/resource/[resourceId]/components/AccessToken.tsx new file mode 100644 index 0000000..d4e35ea --- /dev/null +++ b/src/app/auth/resource/[resourceId]/components/AccessToken.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { createApiClient } from "@app/api"; +import { Button } from "@app/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle +} from "@app/components/ui/card"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { AuthWithAccessTokenResponse } from "@server/routers/resource"; +import { AxiosResponse } from "axios"; +import { Loader2 } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +type AccessTokenProps = { + accessTokenId: string | undefined; + accessToken: string | undefined; + resourceId: number; + redirectUrl: string; +}; + +export default function AccessToken({ + accessTokenId, + accessToken, + resourceId, + redirectUrl +}: AccessTokenProps) { + const [loading, setLoading] = useState(true); + + const api = createApiClient(useEnvContext()); + + useEffect(() => { + if (!accessTokenId || !accessToken) { + setLoading(false); + return; + } + + async function check() { + try { + const res = await api.post< + AxiosResponse + >(`/auth/resource/${resourceId}/access-token`, { + accessToken, + accessTokenId + }); + + if (res.data.data.session) { + window.location.href = redirectUrl; + } + } catch (e) { + console.error("Error checking access token", e); + } finally { + setLoading(false); + } + } + + check(); + }, [accessTokenId, accessToken]); + + return loading ? ( +

+ ) : ( + + + + Access URL Invalid + + + + This shared access URL is invalid. Please contact the resource + owner for a new URL. +
+ +
+
+
+ ); +} diff --git a/src/app/auth/resource/[resourceId]/components/AccessTokenInvalid.tsx b/src/app/auth/resource/[resourceId]/components/AccessTokenInvalid.tsx deleted file mode 100644 index f7b82d4..0000000 --- a/src/app/auth/resource/[resourceId]/components/AccessTokenInvalid.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { Button } from "@app/components/ui/button"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle -} from "@app/components/ui/card"; -import Link from "next/link"; - -export default function AccessTokenInvalid() { - return ( - - - - Acess URL Invalid - - - - This shared access URL is invalid. Please contact the resource - owner for a new URL. -
- -
-
-
- ); -} diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 98ad312..aee798c 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -14,7 +14,8 @@ import ResourceNotFound from "./components/ResourceNotFound"; import ResourceAccessDenied from "./components/ResourceAccessDenied"; import { cookies } from "next/headers"; import { CheckResourceSessionResponse } from "@server/routers/auth"; -import AccessTokenInvalid from "./components/AccessTokenInvalid"; +import AccessTokenInvalid from "./components/AccessToken"; +import AccessToken from "./components/AccessToken"; export default async function ResourceAuthPage(props: { params: Promise<{ resourceId: number }>; @@ -50,35 +51,6 @@ export default async function ResourceAuthPage(props: { const redirectUrl = searchParams.redirect || authInfo.url; - if (searchParams.token) { - let doRedirect = false; - try { - const res = await internal.post< - AxiosResponse - >( - `/auth/resource/${params.resourceId}/access-token`, - { - accessToken: searchParams.token - }, - await authCookieHeader() - ); - - if (res.data.data.session) { - doRedirect = true; - } - } catch (e) { - return ( -
- -
- ); - } - - if (doRedirect) { - redirect(redirectUrl); - } - } - const hasAuth = authInfo.password || authInfo.pincode || @@ -146,6 +118,20 @@ export default async function ResourceAuthPage(props: { } } + if (searchParams.token) { + const [accessTokenId, accessToken] = searchParams.token.split("."); + return ( +
+ +
+ ); + } + return ( <> {userIsUnauthorized && isSSOOnly ? (