fix issues from test deploy

This commit is contained in:
Milo Schwartz 2024-12-21 21:01:12 -05:00
parent 3fb3be1f1e
commit ce5df3b0b9
No known key found for this signature in database
92 changed files with 1410 additions and 1019 deletions

View file

@ -25,7 +25,8 @@ gerbil:
block_size: 16 block_size: 16
subnet_group: 10.0.0.0/8 subnet_group: 10.0.0.0/8
rate_limit: rate_limits:
global:
window_minutes: 1 window_minutes: 1
max_requests: 100 max_requests: 100

View file

@ -38,9 +38,9 @@ export function createApiServer() {
if (!dev) { if (!dev) {
apiServer.use( apiServer.use(
rateLimitMiddleware({ rateLimitMiddleware({
windowMin: config.rate_limit.window_minutes, windowMin: config.rate_limits.global.window_minutes,
max: config.rate_limit.max_requests, max: config.rate_limits.global.max_requests,
type: "IP_ONLY", type: "IP_AND_PATH",
}), }),
); );
} }

View file

@ -16,7 +16,7 @@ const environmentSchema = z.object({
app: z.object({ app: z.object({
base_url: z.string().url(), base_url: z.string().url(),
log_level: z.enum(["debug", "info", "warn", "error"]), log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.boolean(), save_logs: z.boolean()
}), }),
server: z.object({ server: z.object({
external_port: portSchema, external_port: portSchema,
@ -26,24 +26,32 @@ const environmentSchema = z.object({
secure_cookies: z.boolean(), secure_cookies: z.boolean(),
signup_secret: z.string().optional(), signup_secret: z.string().optional(),
session_cookie_name: z.string(), session_cookie_name: z.string(),
resource_session_cookie_name: z.string(), resource_session_cookie_name: z.string()
}), }),
traefik: z.object({ traefik: z.object({
http_entrypoint: z.string(), http_entrypoint: z.string(),
https_entrypoint: z.string().optional(), https_entrypoint: z.string().optional(),
cert_resolver: z.string().optional(), cert_resolver: z.string().optional(),
prefer_wildcard_cert: z.boolean().optional(), prefer_wildcard_cert: z.boolean().optional()
}), }),
gerbil: z.object({ gerbil: z.object({
start_port: portSchema, start_port: portSchema,
base_endpoint: z.string(), base_endpoint: z.string(),
use_subdomain: z.boolean(), use_subdomain: z.boolean(),
subnet_group: z.string(), subnet_group: z.string(),
block_size: z.number().positive().gt(0), block_size: z.number().positive().gt(0)
}), }),
rate_limit: z.object({ rate_limits: z.object({
global: z.object({
window_minutes: z.number().positive().gt(0), window_minutes: z.number().positive().gt(0),
max_requests: 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 email: z
.object({ .object({
@ -51,7 +59,7 @@ const environmentSchema = z.object({
smtp_port: portSchema.optional(), smtp_port: portSchema.optional(),
smtp_user: z.string().optional(), smtp_user: z.string().optional(),
smtp_pass: z.string().optional(), smtp_pass: z.string().optional(),
no_reply: z.string().email().optional(), no_reply: z.string().email().optional()
}) })
.optional(), .optional(),
flags: z flags: z
@ -59,9 +67,9 @@ const environmentSchema = z.object({
allow_org_subdomain_changing: z.boolean().optional(), allow_org_subdomain_changing: z.boolean().optional(),
require_email_verification: z.boolean().optional(), require_email_verification: z.boolean().optional(),
disable_signup_without_invite: 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) => { const loadConfig = (configPath: string) => {
@ -72,7 +80,7 @@ const loadConfig = (configPath: string) => {
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
throw new Error( throw new Error(
`Error loading configuration file: ${error.message}`, `Error loading configuration file: ${error.message}`
); );
} }
throw error; throw error;
@ -94,21 +102,21 @@ if (!environment) {
try { try {
const exampleConfigContent = fs.readFileSync( const exampleConfigContent = fs.readFileSync(
exampleConfigPath, exampleConfigPath,
"utf8", "utf8"
); );
fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8"); fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8");
environment = loadConfig(configFilePath1); environment = loadConfig(configFilePath1);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
throw new Error( throw new Error(
`Error creating configuration file from example: ${error.message}`, `Error creating configuration file from example: ${error.message}`
); );
} }
throw error; throw error;
} }
} else { } else {
throw new Error( throw new Error(
"No configuration file found and no example configuration available", "No configuration file found and no example configuration available"
); );
} }
} }

View file

@ -9,9 +9,11 @@ import { resourceAccessToken } from "@server/db/schema";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import db from "@server/db"; import db from "@server/db";
const deleteAccessTokenParamsSchema = z.object({ const deleteAccessTokenParamsSchema = z
.object({
accessTokenId: z.string() accessTokenId: z.string()
}); })
.strict();
export async function deleteAccessToken( export async function deleteAccessToken(
req: Request, req: Request,

View file

@ -5,7 +5,11 @@ import {
SESSION_COOKIE_EXPIRES SESSION_COOKIE_EXPIRES
} from "@server/auth"; } from "@server/auth";
import db from "@server/db"; 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 HttpCode from "@server/types/HttpCode";
import response from "@server/utils/response"; import response from "@server/utils/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@ -16,17 +20,27 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { createDate, TimeSpan } from "oslo"; import { createDate, TimeSpan } from "oslo";
export const generateAccessTokenBodySchema = z.object({ export const generateAccessTokenBodySchema = z
.object({
validForSeconds: z.number().int().positive().optional(), // seconds validForSeconds: z.number().int().positive().optional(), // seconds
title: z.string().optional(), title: z.string().optional(),
description: z.string().optional() description: z.string().optional()
}); })
.strict();
export const generateAccssTokenParamsSchema = z.object({ export const generateAccssTokenParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) .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( export async function generateAccessToken(
req: Request, req: Request,
@ -77,25 +91,38 @@ export async function generateAccessToken(
const token = generateIdFromEntropySize(25); const token = generateIdFromEntropySize(25);
// const tokenHash = await hash(token, { const tokenHash = await hash(token, {
// memoryCost: 19456, memoryCost: 19456,
// timeCost: 2, timeCost: 2,
// outputLen: 32, outputLen: 32,
// parallelism: 1 parallelism: 1
// }); });
const id = generateId(15); const id = generateId(15);
const [result] = await db.insert(resourceAccessToken).values({ const [result] = await db
.insert(resourceAccessToken)
.values({
accessTokenId: id, accessTokenId: id,
orgId: resource.orgId, orgId: resource.orgId,
resourceId, resourceId,
tokenHash: token, tokenHash,
expiresAt: expiresAt || null, expiresAt: expiresAt || null,
sessionLength: sessionLength, sessionLength: sessionLength,
title: title || null, title: title || null,
description: description || null, description: description || null,
createdAt: new Date().getTime() createdAt: new Date().getTime()
}).returning(); })
.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) { if (!result) {
return next( return next(
@ -107,7 +134,7 @@ export async function generateAccessToken(
} }
return response<GenerateAccessTokenResponse>(res, { return response<GenerateAccessTokenResponse>(res, {
data: result, data: { ...result, accessToken: token },
success: true, success: true,
error: false, error: false,
message: "Resource access token generated successfully", message: "Resource access token generated successfully",

View file

@ -23,6 +23,7 @@ const listAccessTokensParamsSchema = z
.pipe(z.number().int().positive().optional()), .pipe(z.number().int().positive().optional()),
orgId: z.string().optional() orgId: z.string().optional()
}) })
.strict()
.refine((data) => !!data.resourceId !== !!data.orgId, { .refine((data) => !!data.resourceId !== !!data.orgId, {
message: "Either resourceId or orgId must be provided, but not both" message: "Either resourceId or orgId must be provided, but not both"
}); });
@ -65,7 +66,10 @@ function queryAccessTokens(
return db return db
.select(cols) .select(cols)
.from(resourceAccessToken) .from(resourceAccessToken)
.leftJoin(resources, eq(resourceAccessToken.resourceId, resources.resourceId)) .leftJoin(
resources,
eq(resourceAccessToken.resourceId, resources.resourceId)
)
.where( .where(
and( and(
inArray( inArray(
@ -83,7 +87,10 @@ function queryAccessTokens(
return db return db
.select(cols) .select(cols)
.from(resourceAccessToken) .from(resourceAccessToken)
.leftJoin(resources, eq(resourceAccessToken.resourceId, resources.resourceId)) .leftJoin(
resources,
eq(resourceAccessToken.resourceId, resources.resourceId)
)
.where( .where(
and( and(
inArray( inArray(

View file

@ -11,12 +11,13 @@ import { response } from "@server/utils";
import { hashPassword, verifyPassword } from "@server/auth/password"; import { hashPassword, verifyPassword } from "@server/auth/password";
import { verifyTotpCode } from "@server/auth/2fa"; import { verifyTotpCode } from "@server/auth/2fa";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
import logger from "@server/logger";
export const changePasswordBody = z.object({ export const changePasswordBody = z.object({
oldPassword: z.string(), oldPassword: z.string(),
newPassword: passwordSchema, newPassword: passwordSchema,
code: z.string().optional(), code: z.string().optional(),
}); }).strict();
export type ChangePasswordBody = z.infer<typeof changePasswordBody>; export type ChangePasswordBody = z.infer<typeof changePasswordBody>;
@ -108,6 +109,7 @@ export async function changePassword(
status: HttpCode.OK, status: HttpCode.OK,
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -5,11 +5,12 @@ import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/utils"; import { response } from "@server/utils";
import { validateResourceSessionToken } from "@server/auth/resource"; import { validateResourceSessionToken } from "@server/auth/resource";
import logger from "@server/logger";
export const params = z.object({ export const params = z.object({
token: z.string(), token: z.string(),
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
}); }).strict();
export type CheckResourceSessionParams = z.infer<typeof params>; export type CheckResourceSessionParams = z.infer<typeof params>;
@ -54,6 +55,7 @@ export async function checkResourceSession(
status: HttpCode.OK, status: HttpCode.OK,
}); });
} catch (e) { } catch (e) {
logger.error(e);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -10,11 +10,12 @@ import { eq } from "drizzle-orm";
import { response } from "@server/utils"; import { response } from "@server/utils";
import { verifyPassword } from "@server/auth/password"; import { verifyPassword } from "@server/auth/password";
import { verifyTotpCode } from "@server/auth/2fa"; import { verifyTotpCode } from "@server/auth/2fa";
import logger from "@server/logger";
export const disable2faBody = z.object({ export const disable2faBody = z.object({
password: z.string(), password: z.string(),
code: z.string().optional(), code: z.string().optional(),
}); }).strict();
export type Disable2faBody = z.infer<typeof disable2faBody>; export type Disable2faBody = z.infer<typeof disable2faBody>;
@ -100,6 +101,7 @@ export async function disable2fa(
status: HttpCode.OK, status: HttpCode.OK,
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -22,7 +22,7 @@ export const loginBodySchema = z.object({
email: z.string().email(), email: z.string().email(),
password: z.string(), password: z.string(),
code: z.string().optional(), code: z.string().optional(),
}); }).strict();
export type LoginBody = z.infer<typeof loginBodySchema>; export type LoginBody = z.infer<typeof loginBodySchema>;
@ -151,6 +151,7 @@ export async function login(
status: HttpCode.OK, status: HttpCode.OK,
}); });
} catch (e) { } catch (e) {
logger.error(e);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -6,13 +6,13 @@ import logger from "@server/logger";
import { import {
createBlankSessionTokenCookie, createBlankSessionTokenCookie,
invalidateSession, invalidateSession,
SESSION_COOKIE_NAME, SESSION_COOKIE_NAME
} from "@server/auth"; } from "@server/auth";
export async function logout( export async function logout(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
const sessionId = req.cookies[SESSION_COOKIE_NAME]; const sessionId = req.cookies[SESSION_COOKIE_NAME];
@ -20,8 +20,8 @@ export async function logout(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, 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, success: true,
error: false, error: false,
message: "Logged out successfully", message: "Logged out successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error("Failed to log out", error); logger.error(error);
return next( return next(
createHttpError( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to log out")
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to log out",
),
); );
} }
} }

View file

@ -3,8 +3,9 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/utils"; import { response } from "@server/utils";
import { User } from "@server/db/schema"; import { User } from "@server/db/schema";
import { sendEmailVerificationCode } from "./sendEmailVerificationCode"; import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode";
import config from "@server/config"; import config from "@server/config";
import logger from "@server/logger";
export type RequestEmailVerificationCodeResponse = { export type RequestEmailVerificationCodeResponse = {
codeSent: boolean; codeSent: boolean;
@ -40,14 +41,15 @@ export async function requestEmailVerificationCode(
return response<RequestEmailVerificationCodeResponse>(res, { return response<RequestEmailVerificationCodeResponse>(res, {
data: { data: {
codeSent: true, codeSent: true
}, },
status: HttpCode.OK, status: HttpCode.OK,
success: true, success: true,
error: false, error: false,
message: `Email verification code sent to ${user.email}`, message: `Email verification code sent to ${user.email}`
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -16,7 +16,7 @@ import { TimeSpan } from "oslo";
export const requestPasswordResetBody = z.object({ export const requestPasswordResetBody = z.object({
email: z.string().email(), email: z.string().email(),
}); }).strict();
export type RequestPasswordResetBody = z.infer<typeof requestPasswordResetBody>; export type RequestPasswordResetBody = z.infer<typeof requestPasswordResetBody>;
@ -87,6 +87,7 @@ export async function requestPasswordReset(
status: HttpCode.OK, status: HttpCode.OK,
}); });
} catch (e) { } catch (e) {
logger.error(e);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -12,10 +12,13 @@ import { eq } from "drizzle-orm";
import { verify } from "@node-rs/argon2"; import { verify } from "@node-rs/argon2";
import { createTOTPKeyURI } from "oslo/otp"; import { createTOTPKeyURI } from "oslo/otp";
import config from "@server/config"; import config from "@server/config";
import logger from "@server/logger";
export const requestTotpSecretBody = z.object({ export const requestTotpSecretBody = z
password: z.string(), .object({
}); password: z.string()
})
.strict();
export type RequestTotpSecretBody = z.infer<typeof requestTotpSecretBody>; export type RequestTotpSecretBody = z.infer<typeof requestTotpSecretBody>;
@ -26,7 +29,7 @@ export type RequestTotpSecretResponse = {
export async function requestTotpSecret( export async function requestTotpSecret(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
const parsedBody = requestTotpSecretBody.safeParse(req.body); const parsedBody = requestTotpSecretBody.safeParse(req.body);
@ -34,8 +37,8 @@ export async function requestTotpSecret(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString(), fromError(parsedBody.error).toString()
), )
); );
} }
@ -48,7 +51,7 @@ export async function requestTotpSecret(
memoryCost: 19456, memoryCost: 19456,
timeCost: 2, timeCost: 2,
outputLen: 32, outputLen: 32,
parallelism: 1, parallelism: 1
}); });
if (!validPassword) { if (!validPassword) {
return next(unauthorized()); return next(unauthorized());
@ -58,8 +61,8 @@ export async function requestTotpSecret(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, 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 await db
.update(users) .update(users)
.set({ .set({
twoFactorSecret: secret, twoFactorSecret: secret
}) })
.where(eq(users.userId, user.userId)); .where(eq(users.userId, user.userId));
return response<RequestTotpSecretResponse>(res, { return response<RequestTotpSecretResponse>(res, {
data: { data: {
secret: uri, secret: uri
}, },
success: true, success: true,
error: false, error: false,
message: "TOTP secret generated successfully", message: "TOTP secret generated successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"Failed to generate TOTP secret", "Failed to generate TOTP secret"
), )
); );
} }
} }

View file

@ -14,12 +14,15 @@ import { passwordSchema } from "@server/auth/passwordSchema";
import { encodeHex } from "oslo/encoding"; import { encodeHex } from "oslo/encoding";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
import { invalidateAllSessions } from "@server/auth"; import { invalidateAllSessions } from "@server/auth";
import logger from "@server/logger";
export const resetPasswordBody = z.object({ export const resetPasswordBody = z
.object({
token: z.string(), token: z.string(),
newPassword: passwordSchema, newPassword: passwordSchema,
code: z.string().optional(), code: z.string().optional()
}); })
.strict();
export type ResetPasswordBody = z.infer<typeof resetPasswordBody>; export type ResetPasswordBody = z.infer<typeof resetPasswordBody>;
@ -30,7 +33,7 @@ export type ResetPasswordResponse = {
export async function resetPassword( export async function resetPassword(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
const parsedBody = resetPasswordBody.safeParse(req.body); const parsedBody = resetPasswordBody.safeParse(req.body);
@ -38,8 +41,8 @@ export async function resetPassword(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString(), fromError(parsedBody.error).toString()
), )
); );
} }
@ -47,7 +50,7 @@ export async function resetPassword(
try { try {
const tokenHash = encodeHex( const tokenHash = encodeHex(
await sha256(new TextEncoder().encode(token)), await sha256(new TextEncoder().encode(token))
); );
const resetRequest = await db const resetRequest = await db
@ -63,8 +66,8 @@ export async function resetPassword(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, 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( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"User not found", "User not found"
), )
); );
} }
@ -89,22 +92,22 @@ export async function resetPassword(
success: true, success: true,
error: false, error: false,
message: "Two-factor authentication required", message: "Two-factor authentication required",
status: HttpCode.ACCEPTED, status: HttpCode.ACCEPTED
}); });
} }
const validOTP = await verifyTotpCode( const validOTP = await verifyTotpCode(
code!, code!,
user[0].twoFactorSecret!, user[0].twoFactorSecret!,
user[0].userId, user[0].userId
); );
if (!validOTP) { if (!validOTP) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Invalid two-factor authentication code", "Invalid two-factor authentication code"
), )
); );
} }
} }
@ -129,14 +132,15 @@ export async function resetPassword(
success: true, success: true,
error: false, error: false,
message: "Password reset successfully", message: "Password reset successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (e) { } catch (e) {
logger.error(e);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"Failed to reset password", "Failed to reset password"
), )
); );
} }
} }

View file

@ -8,7 +8,7 @@ import { fromError } from "zod-validation-error";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import response from "@server/utils/response"; import response from "@server/utils/response";
import { SqliteError } from "better-sqlite3"; import { SqliteError } from "better-sqlite3";
import { sendEmailVerificationCode } from "./sendEmailVerificationCode"; import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import moment from "moment"; import moment from "moment";
@ -20,6 +20,7 @@ import {
} from "@server/auth"; } from "@server/auth";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import config from "@server/config"; import config from "@server/config";
import logger from "@server/logger";
export const signupBodySchema = z.object({ export const signupBodySchema = z.object({
email: z.string().email(), email: z.string().email(),
@ -153,6 +154,7 @@ export async function signup(
) )
); );
} else { } else {
logger.error(e);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -9,10 +9,13 @@ import { User, emailVerificationCodes, users } from "@server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
import config from "@server/config"; import config from "@server/config";
import logger from "@server/logger";
export const verifyEmailBody = z.object({ export const verifyEmailBody = z
code: z.string(), .object({
}); code: z.string()
})
.strict();
export type VerifyEmailBody = z.infer<typeof verifyEmailBody>; export type VerifyEmailBody = z.infer<typeof verifyEmailBody>;
@ -66,7 +69,7 @@ export async function verifyEmail(
await db await db
.update(users) .update(users)
.set({ .set({
emailVerified: true, emailVerified: true
}) })
.where(eq(users.userId, user.userId)); .where(eq(users.userId, user.userId));
} else { } else {
@ -84,10 +87,11 @@ export async function verifyEmail(
message: "Email verified", message: "Email verified",
status: HttpCode.OK, status: HttpCode.OK,
data: { data: {
valid, valid
}, }
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -10,10 +10,13 @@ import { eq } from "drizzle-orm";
import { alphabet, generateRandomString } from "oslo/crypto"; import { alphabet, generateRandomString } from "oslo/crypto";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { verifyTotpCode } from "@server/auth/2fa"; import { verifyTotpCode } from "@server/auth/2fa";
import logger from "@server/logger";
export const verifyTotpBody = z.object({ export const verifyTotpBody = z
code: z.string(), .object({
}); code: z.string()
})
.strict();
export type VerifyTotpBody = z.infer<typeof verifyTotpBody>; export type VerifyTotpBody = z.infer<typeof verifyTotpBody>;
@ -82,7 +85,7 @@ export async function verifyTotp(
await db.insert(twoFactorBackupCodes).values({ await db.insert(twoFactorBackupCodes).values({
userId: user.userId, userId: user.userId,
codeHash: hash, codeHash: hash
}); });
} }
} }
@ -92,16 +95,17 @@ export async function verifyTotp(
return response<VerifyTotpResponse>(res, { return response<VerifyTotpResponse>(res, {
data: { data: {
valid, valid,
...(valid && codes ? { backupCodes: codes } : {}), ...(valid && codes ? { backupCodes: codes } : {})
}, },
success: true, success: true,
error: false, error: false,
message: valid message: valid
? "Code is valid. Two-factor is now enabled" ? "Code is valid. Two-factor is now enabled"
: "Code is invalid", : "Code is invalid",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -139,39 +139,36 @@ export async function verifyResourceSession(
); );
if (resourceSession) { if (resourceSession) {
if (pincode && resourceSession.pincodeId) {
logger.debug(
"Resource allowed because pincode session is valid"
);
return allowed(res); return allowed(res);
}
// Might not be needed if (password && resourceSession.passwordId) {
// if (pincode && resourceSession.pincodeId) { logger.debug(
// logger.debug( "Resource allowed because password session is valid"
// "Resource allowed because pincode session is valid" );
// ); return allowed(res);
// return allowed(res); }
// }
// if (
// if (password && resourceSession.passwordId) { resource.emailWhitelistEnabled &&
// logger.debug( resourceSession.whitelistId
// "Resource allowed because password session is valid" ) {
// ); logger.debug(
// return allowed(res); "Resource allowed because whitelist session is valid"
// } );
// return allowed(res);
// if ( }
// resource.emailWhitelistEnabled &&
// resourceSession.whitelistId if (resourceSession.accessTokenId) {
// ) { logger.debug(
// logger.debug( "Resource allowed because access token session is valid"
// "Resource allowed because whitelist session is valid" );
// ); return allowed(res);
// return allowed(res); }
// }
//
// if (resourceSession.accessTokenId) {
// logger.debug(
// "Resource allowed because access token session is valid"
// );
// return allowed(res);
// }
} }
} }

View file

@ -1,4 +1,5 @@
import { Router } from "express"; import { Router } from "express";
import config from "@server/config";
import * as site from "./site"; import * as site from "./site";
import * as org from "./org"; import * as org from "./org";
import * as resource from "./resource"; import * as resource from "./resource";
@ -419,8 +420,12 @@ export const authRouter = Router();
unauthenticated.use("/auth", authRouter); unauthenticated.use("/auth", authRouter);
authRouter.use( authRouter.use(
rateLimitMiddleware({ rateLimitMiddleware({
windowMin: 10, windowMin:
max: 75, 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" type: "IP_AND_PATH"
}) })
); );

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const getOrgSchema = z.object({ const getOrgSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
export async function checkId( export async function checkId(
req: Request, req: Request,
@ -43,7 +45,7 @@ export async function checkId(
success: true, success: true,
error: false, error: false,
message: "Organization ID already exists", message: "Organization ID already exists",
status: HttpCode.OK, status: HttpCode.OK
}); });
} }
@ -52,7 +54,7 @@ export async function checkId(
success: true, success: true,
error: false, error: false,
message: "Organization ID is available", message: "Organization ID is available",
status: HttpCode.NOT_FOUND, status: HttpCode.NOT_FOUND
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -10,9 +10,11 @@ import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const deleteOrgSchema = z.object({ const deleteOrgSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
export async function deleteOrg( export async function deleteOrg(
req: Request, req: Request,
@ -65,7 +67,7 @@ export async function deleteOrg(
success: true, success: true,
error: false, error: false,
message: "Organization deleted successfully", message: "Organization deleted successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -8,9 +8,11 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
const getOrgSchema = z.object({ const getOrgSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
export type GetOrgResponse = { export type GetOrgResponse = {
org: Org; org: Org;
@ -51,12 +53,12 @@ export async function getOrg(
return response<GetOrgResponse>(res, { return response<GetOrgResponse>(res, {
data: { data: {
org: org[0], org: org[0]
}, },
success: true, success: true,
error: false, error: false,
message: "Organization retrieved successfully", message: "Organization retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,18 +9,20 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const updateOrgParamsSchema = z.object({ const updateOrgParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const updateOrgBodySchema = z const updateOrgBodySchema = z
.object({ .object({
name: z.string().min(1).max(255).optional(), 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() .strict()
.refine((data) => Object.keys(data).length > 0, { .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( export async function updateOrg(
@ -72,7 +74,7 @@ export async function updateOrg(
success: true, success: true,
error: false, error: false,
message: "Organization updated successfully", message: "Organization updated successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -17,13 +17,21 @@ import logger from "@server/logger";
import { verify } from "@node-rs/argon2"; import { verify } from "@node-rs/argon2";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
const authWithAccessTokenBodySchema = z.object({ const authWithAccessTokenBodySchema = z
accessToken: z.string() .object({
}); accessToken: z.string(),
accessTokenId: z.string()
})
.strict();
const authWithAccessTokenParamsSchema = z.object({ const authWithAccessTokenParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export type AuthWithAccessTokenResponse = { export type AuthWithAccessTokenResponse = {
session?: string; session?: string;
@ -57,9 +65,7 @@ export async function authWithAccessToken(
} }
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
const { accessToken: at } = parsedBody.data; const { accessToken, accessTokenId } = parsedBody.data;
const [accessTokenId, accessToken] = at.split(".");
try { try {
const [result] = await db const [result] = await db
@ -86,7 +92,7 @@ export async function authWithAccessToken(
HttpCode.UNAUTHORIZED, HttpCode.UNAUTHORIZED,
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, 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, { const validCode = await verify(tokenItem.tokenHash, accessToken, {
// memoryCost: 19456, memoryCost: 19456,
// timeCost: 2, timeCost: 2,
// outputLen: 32, outputLen: 32,
// parallelism: 1 parallelism: 1
// }); });
logger.debug(`${accessToken} ${tokenItem.tokenHash}`)
const validCode = accessToken === tokenItem.tokenHash;
if (!validCode) { if (!validCode) {
return next( return next(
createHttpError(HttpCode.UNAUTHORIZED, "Invalid access token") createHttpError(HttpCode.UNAUTHORIZED, "Invalid access token")

View file

@ -14,14 +14,22 @@ import {
serializeResourceSessionCookie serializeResourceSessionCookie
} from "@server/auth/resource"; } from "@server/auth/resource";
import config from "@server/config"; import config from "@server/config";
import logger from "@server/logger";
export const authWithPasswordBodySchema = z.object({ export const authWithPasswordBodySchema = z
.object({
password: z.string() password: z.string()
}); })
.strict();
export const authWithPasswordParamsSchema = z.object({ export const authWithPasswordParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export type AuthWithPasswordResponse = { export type AuthWithPasswordResponse = {
session?: string; session?: string;
@ -120,10 +128,7 @@ export async function authWithPassword(
passwordId: definedPassword.passwordId passwordId: definedPassword.passwordId
}); });
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
const cookie = serializeResourceSessionCookie( const cookie = serializeResourceSessionCookie(cookieName, token);
cookieName,
token,
);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
return response<AuthWithPasswordResponse>(res, { return response<AuthWithPasswordResponse>(res, {
@ -136,6 +141,7 @@ export async function authWithPassword(
status: HttpCode.OK status: HttpCode.OK
}); });
} catch (e) { } catch (e) {
logger.error(e);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -24,13 +24,20 @@ import config from "@server/config";
import { AuthWithPasswordResponse } from "./authWithPassword"; import { AuthWithPasswordResponse } from "./authWithPassword";
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp"; import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
export const authWithPincodeBodySchema = z.object({ export const authWithPincodeBodySchema = z
.object({
pincode: z.string() pincode: z.string()
}); })
.strict();
export const authWithPincodeParamsSchema = z.object({ export const authWithPincodeParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export type AuthWithPincodeResponse = { export type AuthWithPincodeResponse = {
session?: string; session?: string;
@ -128,10 +135,7 @@ export async function authWithPincode(
pincodeId: definedPincode.pincodeId pincodeId: definedPincode.pincodeId
}); });
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
const cookie = serializeResourceSessionCookie( const cookie = serializeResourceSessionCookie(cookieName, token);
cookieName,
token,
);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
return response<AuthWithPincodeResponse>(res, { return response<AuthWithPincodeResponse>(res, {

View file

@ -22,14 +22,21 @@ import config from "@server/config";
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp"; import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
import logger from "@server/logger"; import logger from "@server/logger";
const authWithWhitelistBodySchema = z.object({ const authWithWhitelistBodySchema = z
.object({
email: z.string().email(), email: z.string().email(),
otp: z.string().optional() otp: z.string().optional()
}); })
.strict();
const authWithWhitelistParamsSchema = z.object({ const authWithWhitelistParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export type AuthWithWhitelistResponse = { export type AuthWithWhitelistResponse = {
otpSent?: boolean; otpSent?: boolean;
@ -171,10 +178,7 @@ export async function authWithWhitelist(
whitelistId: whitelistedEmail.whitelistId whitelistId: whitelistedEmail.whitelistId
}); });
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
const cookie = serializeResourceSessionCookie( const cookie = serializeResourceSessionCookie(cookieName, token);
cookieName,
token,
);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
return response<AuthWithWhitelistResponse>(res, { return response<AuthWithWhitelistResponse>(res, {

View file

@ -1,3 +1,4 @@
import { SqliteError } from "better-sqlite3";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
@ -7,7 +8,7 @@ import {
resources, resources,
roleResources, roleResources,
roles, roles,
userResources, userResources
} from "@server/db/schema"; } from "@server/db/schema";
import response from "@server/utils/response"; import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -16,16 +17,19 @@ import { eq, and } from "drizzle-orm";
import stoi from "@server/utils/stoi"; import stoi from "@server/utils/stoi";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/schemas/subdomainSchema"; import { subdomainSchema } from "@server/schemas/subdomainSchema";
import logger from "@server/logger";
const createResourceParamsSchema = z.object({ const createResourceParamsSchema = z
.object({
siteId: z.string().transform(stoi).pipe(z.number().int().positive()), siteId: z.string().transform(stoi).pipe(z.number().int().positive()),
orgId: z.string(), orgId: z.string()
}); })
.strict();
const createResourceSchema = z const createResourceSchema = z
.object({ .object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
subdomain: subdomainSchema, subdomain: subdomainSchema
}) })
.strict(); .strict();
@ -94,7 +98,7 @@ export async function createResource(
orgId, orgId,
name, name,
subdomain, subdomain,
ssl: true, ssl: true
}) })
.returning(); .returning();
@ -112,14 +116,14 @@ export async function createResource(
await db.insert(roleResources).values({ await db.insert(roleResources).values({
roleId: adminRole[0].roleId, roleId: adminRole[0].roleId,
resourceId: newResource[0].resourceId, resourceId: newResource[0].resourceId
}); });
if (req.userOrgRoleId != adminRole[0].roleId) { if (req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the resource // make sure the user can access the resource
await db.insert(userResources).values({ await db.insert(userResources).values({
userId: req.user?.userId!, userId: req.user?.userId!,
resourceId: newResource[0].resourceId, resourceId: newResource[0].resourceId
}); });
} }
@ -128,9 +132,22 @@ export async function createResource(
success: true, success: true,
error: false, error: false,
message: "Resource created successfully", message: "Resource created successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } 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( return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );

View file

@ -12,9 +12,14 @@ import { addPeer } from "../gerbil/peers";
import { removeTargets } from "../newt/targets"; import { removeTargets } from "../newt/targets";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const deleteResourceSchema = z.object({ const deleteResourceSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export async function deleteResource( export async function deleteResource(
req: Request, req: Request,
@ -73,14 +78,14 @@ export async function deleteResource(
// TODO: is this all inefficient? // TODO: is this all inefficient?
// Fetch resources for this site // Fetch resources for this site
const resourcesRes = await db.query.resources.findMany({ 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 // Fetch targets for all resources of this site
const targetIps = await Promise.all( const targetIps = await Promise.all(
resourcesRes.map(async (resource) => { resourcesRes.map(async (resource) => {
const targetsRes = await db.query.targets.findMany({ 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`); return targetsRes.map((target) => `${target.ip}/32`);
}) })
@ -88,7 +93,7 @@ export async function deleteResource(
await addPeer(site.exitNodeId!, { await addPeer(site.exitNodeId!, {
publicKey: site.pubKey, publicKey: site.pubKey,
allowedIps: targetIps.flat(), allowedIps: targetIps.flat()
}); });
} else if (site.type == "newt") { } else if (site.type == "newt") {
// get the newt on the site by querying the newt table for siteId // get the newt on the site by querying the newt table for siteId
@ -107,7 +112,7 @@ export async function deleteResource(
success: true, success: true,
error: false, error: false,
message: "Resource deleted successfully", message: "Resource deleted successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -7,10 +7,16 @@ import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger";
const getResourceSchema = z.object({ const getResourceSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export type GetResourceResponse = Resource; export type GetResourceResponse = Resource;
@ -52,9 +58,10 @@ export async function getResource(
success: true, success: true,
error: false, error: false,
message: "Resource retrieved successfully", message: "Resource retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );

View file

@ -4,17 +4,23 @@ import { db } from "@server/db";
import { import {
resourcePassword, resourcePassword,
resourcePincode, resourcePincode,
resources, resources
} from "@server/db/schema"; } from "@server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/utils/response"; import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger";
const getResourceAuthInfoSchema = z.object({ const getResourceAuthInfoSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export type GetResourceAuthInfoResponse = { export type GetResourceAuthInfoResponse = {
resourceId: number; resourceId: number;
@ -30,7 +36,7 @@ export type GetResourceAuthInfoResponse = {
export async function getResourceAuthInfo( export async function getResourceAuthInfo(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedParams = getResourceAuthInfoSchema.safeParse(req.params); const parsedParams = getResourceAuthInfoSchema.safeParse(req.params);
@ -38,8 +44,8 @@ export async function getResourceAuthInfo(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString(), fromError(parsedParams.error).toString()
), )
); );
} }
@ -50,11 +56,11 @@ export async function getResourceAuthInfo(
.from(resources) .from(resources)
.leftJoin( .leftJoin(
resourcePincode, resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId), eq(resourcePincode.resourceId, resources.resourceId)
) )
.leftJoin( .leftJoin(
resourcePassword, resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId), eq(resourcePassword.resourceId, resources.resourceId)
) )
.where(eq(resources.resourceId, resourceId)) .where(eq(resources.resourceId, resourceId))
.limit(1); .limit(1);
@ -67,7 +73,7 @@ export async function getResourceAuthInfo(
if (!resource) { if (!resource) {
return next( 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, success: true,
error: false, error: false,
message: "Resource auth info retrieved successfully", message: "Resource auth info retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred",
),
); );
} }
} }

View file

@ -9,9 +9,14 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const getResourceWhitelistSchema = z.object({ const getResourceWhitelistSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
async function queryWhitelist(resourceId: number) { async function queryWhitelist(resourceId: number) {
return await db return await db

View file

@ -9,9 +9,14 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const listResourceRolesSchema = z.object({ const listResourceRolesSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
async function query(resourceId: number) { async function query(resourceId: number) {
return await db return await db
@ -19,7 +24,7 @@ async function query(resourceId: number) {
roleId: roles.roleId, roleId: roles.roleId,
name: roles.name, name: roles.name,
description: roles.description, description: roles.description,
isAdmin: roles.isAdmin, isAdmin: roles.isAdmin
}) })
.from(roleResources) .from(roleResources)
.innerJoin(roles, eq(roleResources.roleId, roles.roleId)) .innerJoin(roles, eq(roleResources.roleId, roles.roleId))
@ -52,12 +57,12 @@ export async function listResourceRoles(
return response<ListResourceRolesResponse>(res, { return response<ListResourceRolesResponse>(res, {
data: { data: {
roles: resourceRolesList, roles: resourceRolesList
}, },
success: true, success: true,
error: false, error: false,
message: "Resource roles retrieved successfully", message: "Resource roles retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,15 +9,20 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const listResourceUsersSchema = z.object({ const listResourceUsersSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
async function queryUsers(resourceId: number) { async function queryUsers(resourceId: number) {
return await db return await db
.select({ .select({
userId: userResources.userId, userId: userResources.userId,
email: users.email, email: users.email
}) })
.from(userResources) .from(userResources)
.innerJoin(users, eq(userResources.userId, users.userId)) .innerJoin(users, eq(userResources.userId, users.userId))
@ -50,12 +55,12 @@ export async function listResourceUsers(
return response<ListResourceUsersResponse>(res, { return response<ListResourceUsersResponse>(res, {
data: { data: {
users: resourceUsersList, users: resourceUsersList
}, },
success: true, success: true,
error: false, error: false,
message: "Resource users retrieved successfully", message: "Resource users retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -7,7 +7,7 @@ import {
userResources, userResources,
roleResources, roleResources,
resourcePassword, resourcePassword,
resourcePincode, resourcePincode
} from "@server/db/schema"; } from "@server/db/schema";
import response from "@server/utils/response"; import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -23,10 +23,11 @@ const listResourcesParamsSchema = z
.optional() .optional()
.transform(stoi) .transform(stoi)
.pipe(z.number().int().positive().optional()), .pipe(z.number().int().positive().optional()),
orgId: z.string().optional(), orgId: z.string().optional()
}) })
.strict()
.refine((data) => !!data.siteId !== !!data.orgId, { .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({ const listResourcesSchema = z.object({
@ -42,13 +43,13 @@ const listResourcesSchema = z.object({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.number().int().nonnegative()), .pipe(z.number().int().nonnegative())
}); });
function queryResources( function queryResources(
accessibleResourceIds: number[], accessibleResourceIds: number[],
siteId?: number, siteId?: number,
orgId?: string, orgId?: string
) { ) {
if (siteId) { if (siteId) {
return db return db
@ -68,17 +69,17 @@ function queryResources(
.leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin(sites, eq(resources.siteId, sites.siteId))
.leftJoin( .leftJoin(
resourcePassword, resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId), eq(resourcePassword.resourceId, resources.resourceId)
) )
.leftJoin( .leftJoin(
resourcePincode, resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId), eq(resourcePincode.resourceId, resources.resourceId)
) )
.where( .where(
and( and(
inArray(resources.resourceId, accessibleResourceIds), inArray(resources.resourceId, accessibleResourceIds),
eq(resources.siteId, siteId), eq(resources.siteId, siteId)
), )
); );
} else if (orgId) { } else if (orgId) {
return db return db
@ -98,17 +99,17 @@ function queryResources(
.leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin(sites, eq(resources.siteId, sites.siteId))
.leftJoin( .leftJoin(
resourcePassword, resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId), eq(resourcePassword.resourceId, resources.resourceId)
) )
.leftJoin( .leftJoin(
resourcePincode, resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId), eq(resourcePincode.resourceId, resources.resourceId)
) )
.where( .where(
and( and(
inArray(resources.resourceId, accessibleResourceIds), inArray(resources.resourceId, accessibleResourceIds),
eq(resources.orgId, orgId), eq(resources.orgId, orgId)
), )
); );
} }
} }
@ -121,7 +122,7 @@ export type ListResourcesResponse = {
export async function listResources( export async function listResources(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedQuery = listResourcesSchema.safeParse(req.query); const parsedQuery = listResourcesSchema.safeParse(req.query);
@ -129,8 +130,8 @@ export async function listResources(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map((e) => e.message).join(", "), parsedQuery.error.errors.map((e) => e.message).join(", ")
), )
); );
} }
const { limit, offset } = parsedQuery.data; const { limit, offset } = parsedQuery.data;
@ -140,8 +141,8 @@ export async function listResources(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map((e) => e.message).join(", "), parsedParams.error.errors.map((e) => e.message).join(", ")
), )
); );
} }
const { siteId, orgId } = parsedParams.data; const { siteId, orgId } = parsedParams.data;
@ -150,29 +151,29 @@ export async function listResources(
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"User does not have access to this organization", "User does not have access to this organization"
), )
); );
} }
const accessibleResources = await db const accessibleResources = await db
.select({ .select({
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`, resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
}) })
.from(userResources) .from(userResources)
.fullJoin( .fullJoin(
roleResources, roleResources,
eq(userResources.resourceId, roleResources.resourceId), eq(userResources.resourceId, roleResources.resourceId)
) )
.where( .where(
or( or(
eq(userResources.userId, req.user!.userId), eq(userResources.userId, req.user!.userId),
eq(roleResources.roleId, req.userOrgRoleId!), eq(roleResources.roleId, req.userOrgRoleId!)
), )
); );
const accessibleResourceIds = accessibleResources.map( const accessibleResourceIds = accessibleResources.map(
(resource) => resource.resourceId, (resource) => resource.resourceId
); );
let countQuery: any = db let countQuery: any = db
@ -192,21 +193,18 @@ export async function listResources(
pagination: { pagination: {
total: totalCount, total: totalCount,
limit, limit,
offset, offset
}, }
}, },
success: true, success: true,
error: false, error: false,
message: "Resources retrieved successfully", message: "Resources retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next( return next(
createHttpError( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred",
),
); );
} }
} }

View file

@ -8,14 +8,15 @@ import createHttpError from "http-errors";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { hash } from "@node-rs/argon2"; import { hash } from "@node-rs/argon2";
import { response } from "@server/utils"; import { response } from "@server/utils";
import logger from "@server/logger";
const setResourceAuthMethodsParamsSchema = z.object({ 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 const setResourceAuthMethodsBodySchema = z
.object({ .object({
password: z.string().min(4).max(100).nullable(), password: z.string().min(4).max(100).nullable()
}) })
.strict(); .strict();
@ -60,7 +61,7 @@ export async function setResourcePassword(
memoryCost: 19456, memoryCost: 19456,
timeCost: 2, timeCost: 2,
outputLen: 32, outputLen: 32,
parallelism: 1, parallelism: 1
}); });
await trx await trx
@ -74,9 +75,10 @@ export async function setResourcePassword(
success: true, success: true,
error: false, error: false,
message: "Resource password set successfully", message: "Resource password set successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );

View file

@ -9,6 +9,7 @@ import { fromError } from "zod-validation-error";
import { hash } from "@node-rs/argon2"; import { hash } from "@node-rs/argon2";
import { response } from "@server/utils"; import { response } from "@server/utils";
import stoi from "@server/utils/stoi"; import stoi from "@server/utils/stoi";
import logger from "@server/logger";
const setResourceAuthMethodsParamsSchema = z.object({ const setResourceAuthMethodsParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -81,6 +82,7 @@ export async function setResourcePincode(
status: HttpCode.CREATED, status: HttpCode.CREATED,
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,

View file

@ -9,13 +9,20 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and, ne } from "drizzle-orm"; import { eq, and, ne } from "drizzle-orm";
const setResourceRolesBodySchema = z.object({ const setResourceRolesBodySchema = z
roleIds: z.array(z.number().int().positive()), .object({
}); roleIds: z.array(z.number().int().positive())
})
.strict();
const setResourceRolesParamsSchema = z.object({ const setResourceRolesParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export async function setResourceRoles( export async function setResourceRoles(
req: Request, req: Request,
@ -99,7 +106,7 @@ export async function setResourceRoles(
success: true, success: true,
error: false, error: false,
message: "Roles set for resource successfully", message: "Roles set for resource successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
}); });
} catch (error) { } catch (error) {

View file

@ -9,13 +9,20 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
const setUserResourcesBodySchema = z.object({ const setUserResourcesBodySchema = z
userIds: z.array(z.string()), .object({
}); userIds: z.array(z.string())
})
.strict();
const setUserResourcesParamsSchema = z.object({ const setUserResourcesParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export async function setResourceUsers( export async function setResourceUsers(
req: Request, req: Request,
@ -66,7 +73,7 @@ export async function setResourceUsers(
success: true, success: true,
error: false, error: false,
message: "Users set for resource successfully", message: "Users set for resource successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
}); });
} catch (error) { } catch (error) {

View file

@ -9,13 +9,20 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
const setResourceWhitelistBodySchema = z.object({ const setResourceWhitelistBodySchema = z
.object({
emails: z.array(z.string().email()).max(50) emails: z.array(z.string().email()).max(50)
}); })
.strict();
const setResourceWhitelistParamsSchema = z.object({ const setResourceWhitelistParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export async function setResourceWhitelist( export async function setResourceWhitelist(
req: Request, req: Request,

View file

@ -10,9 +10,14 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/schemas/subdomainSchema"; import { subdomainSchema } from "@server/schemas/subdomainSchema";
const updateResourceParamsSchema = z.object({ const updateResourceParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
const updateResourceBodySchema = z const updateResourceBodySchema = z
.object({ .object({
@ -21,18 +26,18 @@ const updateResourceBodySchema = z
ssl: z.boolean().optional(), ssl: z.boolean().optional(),
sso: z.boolean().optional(), sso: z.boolean().optional(),
blockAccess: z.boolean().optional(), blockAccess: z.boolean().optional(),
emailWhitelistEnabled: z.boolean().optional(), emailWhitelistEnabled: z.boolean().optional()
// siteId: z.number(), // siteId: z.number(),
}) })
.strict() .strict()
.refine((data) => Object.keys(data).length > 0, { .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( export async function updateResource(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedParams = updateResourceParamsSchema.safeParse(req.params); const parsedParams = updateResourceParamsSchema.safeParse(req.params);
@ -40,8 +45,8 @@ export async function updateResource(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString(), fromError(parsedParams.error).toString()
), )
); );
} }
@ -50,8 +55,8 @@ export async function updateResource(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString(), fromError(parsedBody.error).toString()
), )
); );
} }
@ -68,8 +73,8 @@ export async function updateResource(
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_FOUND, 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, 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 = { const updatePayload = {
...updateData, ...updateData,
...(fullDomain && { fullDomain }), ...(fullDomain && { fullDomain })
}; };
const updatedResource = await db const updatedResource = await db
@ -101,8 +106,8 @@ export async function updateResource(
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_FOUND, 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, success: true,
error: false, error: false,
message: "Resource updated successfully", message: "Resource updated successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next( return next(
createHttpError( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred",
),
); );
} }
} }

View file

@ -9,13 +9,17 @@ import logger from "@server/logger";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const addRoleActionParamSchema = z.object({ const addRoleActionParamSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const addRoleActionSchema = z.object({ const addRoleActionSchema = z
actionId: z.string(), .object({
}); actionId: z.string()
})
.strict();
export async function addRoleAction( export async function addRoleAction(
req: Request, req: Request,
@ -66,7 +70,7 @@ export async function addRoleAction(
.values({ .values({
roleId, roleId,
actionId, actionId,
orgId: role[0].orgId!, orgId: role[0].orgId!
}) })
.returning(); .returning();
@ -75,7 +79,7 @@ export async function addRoleAction(
success: true, success: true,
error: false, error: false,
message: "Action added to role successfully", message: "Action added to role successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,13 +9,17 @@ import logger from "@server/logger";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const addRoleSiteParamsSchema = z.object({ const addRoleSiteParamsSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const addRoleSiteSchema = z.object({ const addRoleSiteSchema = z
siteId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); siteId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function addRoleSite( export async function addRoleSite(
req: Request, req: Request,
@ -51,7 +55,7 @@ export async function addRoleSite(
.insert(roleSites) .insert(roleSites)
.values({ .values({
roleId, roleId,
siteId, siteId
}) })
.returning(); .returning();
@ -63,7 +67,7 @@ export async function addRoleSite(
for (const resource of siteResources) { for (const resource of siteResources) {
await db.insert(roleResources).values({ await db.insert(roleResources).values({
roleId, roleId,
resourceId: resource.resourceId, resourceId: resource.resourceId
}); });
} }
@ -72,7 +76,7 @@ export async function addRoleSite(
success: true, success: true,
error: false, error: false,
message: "Site added to role successfully", message: "Site added to role successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -10,21 +10,23 @@ import { fromError } from "zod-validation-error";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
const createRoleParamsSchema = z.object({ const createRoleParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const createRoleSchema = z const createRoleSchema = z
.object({ .object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
description: z.string().optional(), description: z.string().optional()
}) })
.strict(); .strict();
export const defaultRoleAllowedActions: ActionsEnum[] = [ export const defaultRoleAllowedActions: ActionsEnum[] = [
ActionsEnum.getOrg, ActionsEnum.getOrg,
ActionsEnum.getResource, ActionsEnum.getResource,
ActionsEnum.listResources, ActionsEnum.listResources
]; ];
export type CreateRoleBody = z.infer<typeof createRoleSchema>; export type CreateRoleBody = z.infer<typeof createRoleSchema>;
@ -64,7 +66,7 @@ export async function createRole(
const allRoles = await db const allRoles = await db
.select({ .select({
roleId: roles.roleId, roleId: roles.roleId,
name: roles.name, name: roles.name
}) })
.from(roles) .from(roles)
.leftJoin(orgs, eq(roles.orgId, orgs.orgId)) .leftJoin(orgs, eq(roles.orgId, orgs.orgId))
@ -84,7 +86,7 @@ export async function createRole(
.insert(roles) .insert(roles)
.values({ .values({
...roleData, ...roleData,
orgId, orgId
}) })
.returning(); .returning();
@ -94,7 +96,7 @@ export async function createRole(
defaultRoleAllowedActions.map((action) => ({ defaultRoleAllowedActions.map((action) => ({
roleId: newRole[0].roleId, roleId: newRole[0].roleId,
actionId: action, actionId: action,
orgId, orgId
})) }))
) )
.execute(); .execute();
@ -104,7 +106,7 @@ export async function createRole(
success: true, success: true,
error: false, error: false,
message: "Role created successfully", message: "Role created successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,13 +9,17 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const deleteRoleSchema = z.object({ const deleteRoleSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const deelteRoleBodySchema = z.object({ const deelteRoleBodySchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function deleteRole( export async function deleteRole(
req: Request, req: Request,
@ -108,7 +112,7 @@ export async function deleteRole(
success: true, success: true,
error: false, error: false,
message: "Role deleted successfully", message: "Role deleted successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const getRoleSchema = z.object({ const getRoleSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function getRole( export async function getRole(
req: Request, req: Request,
@ -51,7 +53,7 @@ export async function getRole(
success: true, success: true,
error: false, error: false,
message: "Role retrieved successfully", message: "Role retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const listRoleActionsSchema = z.object({ const listRoleActionsSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function listRoleActions( export async function listRoleActions(
req: Request, req: Request,
@ -35,7 +37,7 @@ export async function listRoleActions(
.select({ .select({
actionId: actions.actionId, actionId: actions.actionId,
name: actions.name, name: actions.name,
description: actions.description, description: actions.description
}) })
.from(roleActions) .from(roleActions)
.innerJoin(actions, eq(roleActions.actionId, actions.actionId)) .innerJoin(actions, eq(roleActions.actionId, actions.actionId))
@ -48,7 +50,7 @@ export async function listRoleActions(
success: true, success: true,
error: false, error: false,
message: "Role actions retrieved successfully", message: "Role actions retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const listRoleResourcesSchema = z.object({ const listRoleResourcesSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function listRoleResources( export async function listRoleResources(
req: Request, req: Request,
@ -35,7 +37,7 @@ export async function listRoleResources(
.select({ .select({
resourceId: resources.resourceId, resourceId: resources.resourceId,
name: resources.name, name: resources.name,
subdomain: resources.subdomain, subdomain: resources.subdomain
}) })
.from(roleResources) .from(roleResources)
.innerJoin( .innerJoin(
@ -51,7 +53,7 @@ export async function listRoleResources(
success: true, success: true,
error: false, error: false,
message: "Role resources retrieved successfully", message: "Role resources retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const listRoleSitesSchema = z.object({ const listRoleSitesSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function listRoleSites( export async function listRoleSites(
req: Request, req: Request,
@ -34,7 +36,7 @@ export async function listRoleSites(
const roleSitesList = await db const roleSitesList = await db
.select({ .select({
siteId: sites.siteId, siteId: sites.siteId,
name: sites.name, name: sites.name
}) })
.from(roleSites) .from(roleSites)
.innerJoin(sites, eq(roleSites.siteId, sites.siteId)) .innerJoin(sites, eq(roleSites.siteId, sites.siteId))
@ -47,7 +49,7 @@ export async function listRoleSites(
success: true, success: true,
error: false, error: false,
message: "Role sites retrieved successfully", message: "Role sites retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -10,9 +10,11 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import stoi from "@server/utils/stoi"; import stoi from "@server/utils/stoi";
const listRolesParamsSchema = z.object({ const listRolesParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const listRolesSchema = z.object({ const listRolesSchema = z.object({
limit: z limit: z
@ -26,7 +28,7 @@ const listRolesSchema = z.object({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.number().int().nonnegative()), .pipe(z.number().int().nonnegative())
}); });
async function queryRoles(orgId: string, limit: number, offset: number) { 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, isAdmin: roles.isAdmin,
name: roles.name, name: roles.name,
description: roles.description, description: roles.description,
orgName: orgs.name, orgName: orgs.name
}) })
.from(roles) .from(roles)
.leftJoin(orgs, eq(roles.orgId, orgs.orgId)) .leftJoin(orgs, eq(roles.orgId, orgs.orgId))
@ -100,13 +102,13 @@ export async function listRoles(
pagination: { pagination: {
total: totalCount, total: totalCount,
limit, limit,
offset, offset
}, }
}, },
success: true, success: true,
error: false, error: false,
message: "Roles retrieved successfully", message: "Roles retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,13 +9,17 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const removeRoleActionParamsSchema = z.object({ const removeRoleActionParamsSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const removeRoleActionSchema = z.object({ const removeRoleActionSchema = z
actionId: z.string(), .object({
}); actionId: z.string()
})
.strict();
export async function removeRoleAction( export async function removeRoleAction(
req: Request, req: Request,
@ -71,7 +75,7 @@ export async function removeRoleAction(
success: true, success: true,
error: false, error: false,
message: "Action removed from role successfully", message: "Action removed from role successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,13 +9,20 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const removeRoleResourceParamsSchema = z.object({ const removeRoleResourceParamsSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const removeRoleResourceSchema = z.object({ const removeRoleResourceSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export async function removeRoleResource( export async function removeRoleResource(
req: Request, req: Request,
@ -71,7 +78,7 @@ export async function removeRoleResource(
success: true, success: true,
error: false, error: false,
message: "Resource removed from role successfully", message: "Resource removed from role successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,13 +9,17 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const removeRoleSiteParamsSchema = z.object({ const removeRoleSiteParamsSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const removeRoleSiteSchema = z.object({ const removeRoleSiteSchema = z
siteId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); siteId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function removeRoleSite( export async function removeRoleSite(
req: Request, req: Request,
@ -85,7 +89,7 @@ export async function removeRoleSite(
success: true, success: true,
error: false, error: false,
message: "Site removed from role successfully", message: "Site removed from role successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,18 +9,20 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const updateRoleParamsSchema = z.object({ const updateRoleParamsSchema = z
roleId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); roleId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const updateRoleBodySchema = z const updateRoleBodySchema = z
.object({ .object({
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
description: z.string().optional(), description: z.string().optional()
}) })
.strict() .strict()
.refine((data) => Object.keys(data).length > 0, { .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( export async function updateRole(
@ -96,7 +98,7 @@ export async function updateRole(
success: true, success: true,
error: false, error: false,
message: "Role updated successfully", message: "Role updated successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; 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 response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@ -14,9 +14,11 @@ import { hash } from "@node-rs/argon2";
import { newts } from "@server/db/schema"; import { newts } from "@server/db/schema";
import moment from "moment"; import moment from "moment";
const createSiteParamsSchema = z.object({ const createSiteParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const createSiteSchema = z const createSiteSchema = z
.object({ .object({
@ -27,18 +29,13 @@ const createSiteSchema = z
subnet: z.string(), subnet: z.string(),
newtId: z.string().optional(), newtId: z.string().optional(),
secret: z.string().optional(), secret: z.string().optional(),
type: z.string(), type: z.string()
}) })
.strict(); .strict();
export type CreateSiteBody = z.infer<typeof createSiteSchema>; export type CreateSiteBody = z.infer<typeof createSiteSchema>;
export type CreateSiteResponse = { export type CreateSiteResponse = Site;
name: string;
siteId: number;
orgId: string;
niceId: string;
};
export async function createSite( export async function createSite(
req: Request, req: Request,
@ -85,14 +82,14 @@ export async function createSite(
name, name,
niceId, niceId,
subnet, subnet,
type, type
}; };
if (pubKey && type == "wireguard") { if (pubKey && type == "wireguard") {
// we dont add the pubKey for newts because the newt will generate it // we dont add the pubKey for newts because the newt will generate it
payload = { payload = {
...payload, ...payload,
pubKey, pubKey
}; };
} }
@ -112,14 +109,14 @@ export async function createSite(
await db.insert(roleSites).values({ await db.insert(roleSites).values({
roleId: adminRole[0].roleId, roleId: adminRole[0].roleId,
siteId: newSite.siteId, siteId: newSite.siteId
}); });
if (req.userOrgRoleId != adminRole[0].roleId) { if (req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the site // make sure the user can access the site
db.insert(userSites).values({ db.insert(userSites).values({
userId: req.user?.userId!, userId: req.user?.userId!,
siteId: newSite.siteId, siteId: newSite.siteId
}); });
} }
@ -129,14 +126,14 @@ export async function createSite(
memoryCost: 19456, memoryCost: 19456,
timeCost: 2, timeCost: 2,
outputLen: 32, outputLen: 32,
parallelism: 1, parallelism: 1
}); });
await db.insert(newts).values({ await db.insert(newts).values({
newtId: newtId!, newtId: newtId!,
secretHash, secretHash,
siteId: newSite.siteId, siteId: newSite.siteId,
dateCreated: moment().toISOString(), dateCreated: moment().toISOString()
}); });
} else if (type == "wireguard") { } else if (type == "wireguard") {
if (!pubKey) { if (!pubKey) {
@ -149,23 +146,19 @@ export async function createSite(
} }
await addPeer(exitNodeId, { await addPeer(exitNodeId, {
publicKey: pubKey, publicKey: pubKey,
allowedIps: [], allowedIps: []
}); });
} }
return response(res, { return response<CreateSiteResponse>(res, {
data: { data: newSite,
name: newSite.name,
niceId: newSite.niceId,
siteId: newSite.siteId,
orgId: newSite.orgId,
},
success: true, success: true,
error: false, error: false,
message: "Site created successfully", message: "Site created successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );

View file

@ -11,9 +11,11 @@ import { deletePeer } from "../gerbil/peers";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { sendToClient } from "../ws"; import { sendToClient } from "../ws";
const deleteSiteSchema = z.object({ const deleteSiteSchema = z
siteId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); siteId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function deleteSite( export async function deleteSite(
req: Request, req: Request,
@ -60,7 +62,7 @@ export async function deleteSite(
if (deletedNewt) { if (deletedNewt) {
const payload = { const payload = {
type: `newt/terminate`, type: `newt/terminate`,
data: {}, data: {}
}; };
sendToClient(deletedNewt.newtId, payload); sendToClient(deletedNewt.newtId, payload);
@ -79,7 +81,7 @@ export async function deleteSite(
success: true, success: true,
error: false, error: false,
message: "Site deleted successfully", message: "Site deleted successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -10,7 +10,8 @@ import logger from "@server/logger";
import stoi from "@server/utils/stoi"; import stoi from "@server/utils/stoi";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const getSiteSchema = z.object({ const getSiteSchema = z
.object({
siteId: z siteId: z
.string() .string()
.optional() .optional()
@ -18,8 +19,9 @@ const getSiteSchema = z.object({
.pipe(z.number().int().positive().optional()) .pipe(z.number().int().positive().optional())
.optional(), .optional(),
niceId: z.string().optional(), niceId: z.string().optional(),
orgId: z.string().optional(), orgId: z.string().optional()
}); })
.strict();
export type GetSiteResponse = { export type GetSiteResponse = {
siteId: number; siteId: number;
@ -79,15 +81,15 @@ export async function getSite(
siteId: site[0].siteId, siteId: site[0].siteId,
niceId: site[0].niceId, niceId: site[0].niceId,
name: site[0].name, name: site[0].name,
subnet: site[0].subnet, subnet: site[0].subnet
}, },
success: true, success: true,
error: false, error: false,
message: "Site retrieved successfully", message: "Site retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error("Error from getSite: ", error); logger.error(error);
return next( return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const listSiteRolesSchema = z.object({ const listSiteRolesSchema = z
siteId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); siteId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function listSiteRoles( export async function listSiteRoles(
req: Request, req: Request,
@ -36,7 +38,7 @@ export async function listSiteRoles(
roleId: roles.roleId, roleId: roles.roleId,
name: roles.name, name: roles.name,
description: roles.description, description: roles.description,
isAdmin: roles.isAdmin, isAdmin: roles.isAdmin
}) })
.from(roleSites) .from(roleSites)
.innerJoin(roles, eq(roleSites.roleId, roles.roleId)) .innerJoin(roles, eq(roleSites.roleId, roles.roleId))
@ -47,7 +49,7 @@ export async function listSiteRoles(
success: true, success: true,
error: false, error: false,
message: "Site roles retrieved successfully", message: "Site roles retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -1,5 +1,6 @@
import { db } from "@server/db"; import { db } from "@server/db";
import { orgs, roleSites, sites, userSites } from "@server/db/schema"; import { orgs, roleSites, sites, userSites } from "@server/db/schema";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/utils/response"; import response from "@server/utils/response";
import { and, count, eq, inArray, or, sql } from "drizzle-orm"; import { and, count, eq, inArray, or, sql } from "drizzle-orm";
@ -8,9 +9,11 @@ import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const listSitesParamsSchema = z.object({ const listSitesParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const listSitesSchema = z.object({ const listSitesSchema = z.object({
limit: z limit: z
@ -24,7 +27,7 @@ const listSitesSchema = z.object({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.number().int().nonnegative()), .pipe(z.number().int().nonnegative())
}); });
function querySites(orgId: string, accessibleSiteIds: number[]) { function querySites(orgId: string, accessibleSiteIds: number[]) {
@ -39,15 +42,15 @@ function querySites(orgId: string, accessibleSiteIds: number[]) {
megabytesOut: sites.megabytesOut, megabytesOut: sites.megabytesOut,
orgName: orgs.name, orgName: orgs.name,
type: sites.type, type: sites.type,
online: sites.online, online: sites.online
}) })
.from(sites) .from(sites)
.leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .leftJoin(orgs, eq(sites.orgId, orgs.orgId))
.where( .where(
and( and(
inArray(sites.siteId, accessibleSiteIds), inArray(sites.siteId, accessibleSiteIds),
eq(sites.orgId, orgId), eq(sites.orgId, orgId)
), )
); );
} }
@ -59,7 +62,7 @@ export type ListSitesResponse = {
export async function listSites( export async function listSites(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedQuery = listSitesSchema.safeParse(req.query); const parsedQuery = listSitesSchema.safeParse(req.query);
@ -67,8 +70,8 @@ export async function listSites(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedQuery.error), fromError(parsedQuery.error)
), )
); );
} }
const { limit, offset } = parsedQuery.data; const { limit, offset } = parsedQuery.data;
@ -78,8 +81,8 @@ export async function listSites(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedParams.error), fromError(parsedParams.error)
), )
); );
} }
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
@ -88,22 +91,22 @@ export async function listSites(
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"User does not have access to this organization", "User does not have access to this organization"
), )
); );
} }
const accessibleSites = await db const accessibleSites = await db
.select({ .select({
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`, siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
}) })
.from(userSites) .from(userSites)
.fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId)) .fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
.where( .where(
or( or(
eq(userSites.userId, req.user!.userId), eq(userSites.userId, req.user!.userId),
eq(roleSites.roleId, req.userOrgRoleId!), eq(roleSites.roleId, req.userOrgRoleId!)
), )
); );
const accessibleSiteIds = accessibleSites.map((site) => site.siteId); const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
@ -115,8 +118,8 @@ export async function listSites(
.where( .where(
and( and(
inArray(sites.siteId, accessibleSiteIds), inArray(sites.siteId, accessibleSiteIds),
eq(sites.orgId, orgId), eq(sites.orgId, orgId)
), )
); );
const sitesList = await baseQuery.limit(limit).offset(offset); const sitesList = await baseQuery.limit(limit).offset(offset);
@ -129,20 +132,18 @@ export async function listSites(
pagination: { pagination: {
total: totalCount, total: totalCount,
limit, limit,
offset, offset
}, }
}, },
success: true, success: true,
error: false, error: false,
message: "Sites retrieved successfully", message: "Sites retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error);
return next( return next(
createHttpError( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred",
),
); );
} }
} }

View file

@ -9,14 +9,16 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const updateSiteParamsSchema = z.object({ const updateSiteParamsSchema = z
siteId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); siteId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const updateSiteBodySchema = z const updateSiteBodySchema = z
.object({ .object({
name: z.string().min(1).max(255).optional(), 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(), // pubKey: z.string().optional(),
// subnet: z.string().optional(), // subnet: z.string().optional(),
// exitNode: z.number().int().positive().optional(), // exitNode: z.number().int().positive().optional(),
@ -25,7 +27,7 @@ const updateSiteBodySchema = z
}) })
.strict() .strict()
.refine((data) => Object.keys(data).length > 0, { .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( export async function updateSite(
@ -77,7 +79,7 @@ export async function updateSite(
success: true, success: true,
error: false, error: false,
message: "Site updated successfully", message: "Site updated successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -12,9 +12,14 @@ import { isIpInCidr } from "@server/utils/ip";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { addTargets } from "../newt/targets"; import { addTargets } from "../newt/targets";
const createTargetParamsSchema = z.object({ const createTargetParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
const createTargetSchema = z const createTargetSchema = z
.object({ .object({
@ -22,7 +27,7 @@ const createTargetSchema = z
method: z.string().min(1).max(10), method: z.string().min(1).max(10),
port: z.number().int().min(1).max(65535), port: z.number().int().min(1).max(65535),
protocol: z.string().optional(), protocol: z.string().optional(),
enabled: z.boolean().default(true), enabled: z.boolean().default(true)
}) })
.strict(); .strict();
@ -61,7 +66,7 @@ export async function createTarget(
// get the resource // get the resource
const [resource] = await db const [resource] = await db
.select({ .select({
siteId: resources.siteId, siteId: resources.siteId
}) })
.from(resources) .from(resources)
.where(eq(resources.resourceId, resourceId)); .where(eq(resources.resourceId, resourceId));
@ -91,7 +96,10 @@ export async function createTarget(
} }
// make sure the target is within the site subnet // 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -102,7 +110,7 @@ export async function createTarget(
// Fetch resources for this site // Fetch resources for this site
const resourcesRes = await db.query.resources.findMany({ const resourcesRes = await db.query.resources.findMany({
where: eq(resources.siteId, site.siteId), where: eq(resources.siteId, site.siteId)
}); });
// TODO: is this all inefficient? // TODO: is this all inefficient?
@ -112,7 +120,7 @@ export async function createTarget(
await Promise.all( await Promise.all(
resourcesRes.map(async (resource) => { resourcesRes.map(async (resource) => {
const targetsRes = await db.query.targets.findMany({ const targetsRes = await db.query.targets.findMany({
where: eq(targets.resourceId, resource.resourceId), where: eq(targets.resourceId, resource.resourceId)
}); });
targetsRes.forEach((target) => { targetsRes.forEach((target) => {
targetIps.push(`${target.ip}/32`); targetIps.push(`${target.ip}/32`);
@ -147,7 +155,7 @@ export async function createTarget(
resourceId, resourceId,
protocol: "tcp", // hard code for now protocol: "tcp", // hard code for now
internalPort, internalPort,
...targetData, ...targetData
}) })
.returning(); .returning();
@ -155,7 +163,7 @@ export async function createTarget(
if (site.type == "wireguard") { if (site.type == "wireguard") {
await addPeer(site.exitNodeId!, { await addPeer(site.exitNodeId!, {
publicKey: site.pubKey, publicKey: site.pubKey,
allowedIps: targetIps.flat(), allowedIps: targetIps.flat()
}); });
} else if (site.type == "newt") { } else if (site.type == "newt") {
// get the newt on the site by querying the newt table for siteId // get the newt on the site by querying the newt table for siteId
@ -174,7 +182,7 @@ export async function createTarget(
success: true, success: true,
error: false, error: false,
message: "Target created successfully", message: "Target created successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -11,9 +11,11 @@ import { addPeer } from "../gerbil/peers";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { removeTargets } from "../newt/targets"; import { removeTargets } from "../newt/targets";
const deleteTargetSchema = z.object({ const deleteTargetSchema = z
targetId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); targetId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function deleteTarget( export async function deleteTarget(
req: Request, req: Request,
@ -49,7 +51,7 @@ export async function deleteTarget(
// get the resource // get the resource
const [resource] = await db const [resource] = await db
.select({ .select({
siteId: resources.siteId, siteId: resources.siteId
}) })
.from(resources) .from(resources)
.where(eq(resources.resourceId, deletedTarget.resourceId!)); .where(eq(resources.resourceId, deletedTarget.resourceId!));
@ -83,14 +85,14 @@ export async function deleteTarget(
// TODO: is this all inefficient? // TODO: is this all inefficient?
// Fetch resources for this site // Fetch resources for this site
const resourcesRes = await db.query.resources.findMany({ 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 // Fetch targets for all resources of this site
const targetIps = await Promise.all( const targetIps = await Promise.all(
resourcesRes.map(async (resource) => { resourcesRes.map(async (resource) => {
const targetsRes = await db.query.targets.findMany({ 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`); return targetsRes.map((target) => `${target.ip}/32`);
}) })
@ -98,7 +100,7 @@ export async function deleteTarget(
await addPeer(site.exitNodeId!, { await addPeer(site.exitNodeId!, {
publicKey: site.pubKey, publicKey: site.pubKey,
allowedIps: targetIps.flat(), allowedIps: targetIps.flat()
}); });
} else if (site.type == "newt") { } else if (site.type == "newt") {
// get the newt on the site by querying the newt table for siteId // get the newt on the site by querying the newt table for siteId
@ -117,7 +119,7 @@ export async function deleteTarget(
success: true, success: true,
error: false, error: false,
message: "Target deleted successfully", message: "Target deleted successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const getTargetSchema = z.object({ const getTargetSchema = z
targetId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); targetId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
export async function getTarget( export async function getTarget(
req: Request, req: Request,
@ -51,7 +53,7 @@ export async function getTarget(
success: true, success: true,
error: false, error: false,
message: "Target retrieved successfully", message: "Target retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,9 +9,14 @@ import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
const listTargetsParamsSchema = z.object({ const listTargetsParamsSchema = z
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
const listTargetsSchema = z.object({ const listTargetsSchema = z.object({
limit: z limit: z
@ -25,7 +30,7 @@ const listTargetsSchema = z.object({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.number().int().nonnegative()), .pipe(z.number().int().nonnegative())
}); });
function queryTargets(resourceId: number) { function queryTargets(resourceId: number) {
@ -37,7 +42,7 @@ function queryTargets(resourceId: number) {
port: targets.port, port: targets.port,
protocol: targets.protocol, protocol: targets.protocol,
enabled: targets.enabled, enabled: targets.enabled,
resourceId: targets.resourceId, resourceId: targets.resourceId
// resourceName: resources.name, // resourceName: resources.name,
}) })
.from(targets) .from(targets)
@ -97,13 +102,13 @@ export async function listTargets(
pagination: { pagination: {
total: totalCount, total: totalCount,
limit, limit,
offset, offset
}, }
}, },
success: true, success: true,
error: false, error: false,
message: "Targets retrieved successfully", message: "Targets retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -11,20 +11,22 @@ import { fromError } from "zod-validation-error";
import { addPeer } from "../gerbil/peers"; import { addPeer } from "../gerbil/peers";
import { addTargets } from "../newt/targets"; import { addTargets } from "../newt/targets";
const updateTargetParamsSchema = z.object({ const updateTargetParamsSchema = z
targetId: z.string().transform(Number).pipe(z.number().int().positive()), .object({
}); targetId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const updateTargetBodySchema = z const updateTargetBodySchema = z
.object({ .object({
ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete 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(), method: z.string().min(1).max(10).optional(),
port: z.number().int().min(1).max(65535).optional(), port: z.number().int().min(1).max(65535).optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional()
}) })
.strict() .strict()
.refine((data) => Object.keys(data).length > 0, { .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( export async function updateTarget(
@ -74,7 +76,7 @@ export async function updateTarget(
// get the resource // get the resource
const [resource] = await db const [resource] = await db
.select({ .select({
siteId: resources.siteId, siteId: resources.siteId
}) })
.from(resources) .from(resources)
.where(eq(resources.resourceId, updatedTarget.resourceId!)); .where(eq(resources.resourceId, updatedTarget.resourceId!));
@ -107,14 +109,14 @@ export async function updateTarget(
// TODO: is this all inefficient? // TODO: is this all inefficient?
// Fetch resources for this site // Fetch resources for this site
const resourcesRes = await db.query.resources.findMany({ 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 // Fetch targets for all resources of this site
const targetIps = await Promise.all( const targetIps = await Promise.all(
resourcesRes.map(async (resource) => { resourcesRes.map(async (resource) => {
const targetsRes = await db.query.targets.findMany({ 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`); return targetsRes.map((target) => `${target.ip}/32`);
}) })
@ -122,7 +124,7 @@ export async function updateTarget(
await addPeer(site.exitNodeId!, { await addPeer(site.exitNodeId!, {
publicKey: site.pubKey, publicKey: site.pubKey,
allowedIps: targetIps.flat(), allowedIps: targetIps.flat()
}); });
} else if (site.type == "newt") { } else if (site.type == "newt") {
// get the newt on the site by querying the newt table for siteId // get the newt on the site by querying the newt table for siteId
@ -140,7 +142,7 @@ export async function updateTarget(
success: true, success: true,
error: false, error: false,
message: "Target updated successfully", message: "Target updated successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -11,10 +11,12 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
const acceptInviteBodySchema = z.object({ const acceptInviteBodySchema = z
.object({
token: z.string(), token: z.string(),
inviteId: z.string(), inviteId: z.string()
}); })
.strict();
export type AcceptInviteResponse = { export type AcceptInviteResponse = {
accepted: boolean; accepted: boolean;
@ -64,7 +66,7 @@ export async function acceptInvite(
memoryCost: 19456, memoryCost: 19456,
timeCost: 2, timeCost: 2,
outputLen: 32, outputLen: 32,
parallelism: 1, parallelism: 1
}); });
if (!validToken) { if (!validToken) {
return next( return next(
@ -121,7 +123,7 @@ export async function acceptInvite(
await db.insert(userOrgs).values({ await db.insert(userOrgs).values({
userId: existingUser[0].userId, userId: existingUser[0].userId,
orgId: existingInvite[0].orgId, orgId: existingInvite[0].orgId,
roleId: existingInvite[0].roleId, roleId: existingInvite[0].roleId
}); });
// delete the invite // delete the invite
@ -132,7 +134,7 @@ export async function acceptInvite(
success: true, success: true,
error: false, error: false,
message: "Invite accepted", message: "Invite accepted",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,11 +9,13 @@ import logger from "@server/logger";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const addUserActionSchema = z.object({ const addUserActionSchema = z
.object({
userId: z.string(), userId: z.string(),
actionId: z.string(), actionId: z.string(),
orgId: z.string(), orgId: z.string()
}); })
.strict();
export async function addUserAction( export async function addUserAction(
req: Request, req: Request,
@ -52,7 +54,7 @@ export async function addUserAction(
.values({ .values({
userId, userId,
actionId, actionId,
orgId, orgId
}) })
.returning(); .returning();
@ -61,7 +63,7 @@ export async function addUserAction(
success: true, success: true,
error: false, error: false,
message: "Action added to user successfully", message: "Action added to user successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -10,10 +10,12 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import stoi from "@server/utils/stoi"; import stoi from "@server/utils/stoi";
const addUserRoleParamsSchema = z.object({ const addUserRoleParamsSchema = z
.object({
userId: z.string(), userId: z.string(),
roleId: z.string().transform(stoi).pipe(z.number()), roleId: z.string().transform(stoi).pipe(z.number())
}); })
.strict();
export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>; export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
@ -96,7 +98,7 @@ export async function addUserRole(
success: true, success: true,
error: false, error: false,
message: "Role added to user successfully", message: "Role added to user successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,10 +9,12 @@ import logger from "@server/logger";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const addUserSiteSchema = z.object({ const addUserSiteSchema = z
.object({
userId: z.string(), userId: z.string(),
siteId: z.string().transform(Number).pipe(z.number().int().positive()), siteId: z.string().transform(Number).pipe(z.number().int().positive())
}); })
.strict();
export async function addUserSite( export async function addUserSite(
req: Request, req: Request,
@ -36,7 +38,7 @@ export async function addUserSite(
.insert(userSites) .insert(userSites)
.values({ .values({
userId, userId,
siteId, siteId
}) })
.returning(); .returning();
@ -48,7 +50,7 @@ export async function addUserSite(
for (const resource of siteResources) { for (const resource of siteResources) {
await db.insert(userResources).values({ await db.insert(userResources).values({
userId, userId,
resourceId: resource.resourceId, resourceId: resource.resourceId
}); });
} }
@ -57,7 +59,7 @@ export async function addUserSite(
success: true, success: true,
error: false, error: false,
message: "Site added to user successfully", message: "Site added to user successfully",
status: HttpCode.CREATED, status: HttpCode.CREATED
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -19,7 +19,7 @@ async function queryUser(orgId: string, userId: string) {
roleId: userOrgs.roleId, roleId: userOrgs.roleId,
roleName: roles.name, roleName: roles.name,
isOwner: userOrgs.isOwner, isOwner: userOrgs.isOwner,
isAdmin: roles.isAdmin, isAdmin: roles.isAdmin
}) })
.from(userOrgs) .from(userOrgs)
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) .leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
@ -33,10 +33,12 @@ export type GetOrgUserResponse = NonNullable<
Awaited<ReturnType<typeof queryUser>> Awaited<ReturnType<typeof queryUser>>
>; >;
const getOrgUserParamsSchema = z.object({ const getOrgUserParamsSchema = z
.object({
userId: z.string(), userId: z.string(),
orgId: z.string(), orgId: z.string()
}); })
.strict();
export async function getOrgUser( export async function getOrgUser(
req: Request, req: Request,
@ -109,7 +111,7 @@ export async function getOrgUser(
success: true, success: true,
error: false, error: false,
message: "User retrieved successfully", message: "User retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -6,7 +6,6 @@ import { and, eq } from "drizzle-orm";
import response from "@server/utils/response"; import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from "@server/logger"; import logger from "@server/logger";
import { alphabet, generateRandomString } from "oslo/crypto"; import { alphabet, generateRandomString } from "oslo/crypto";
import { createDate, TimeSpan } from "oslo"; import { createDate, TimeSpan } from "oslo";
@ -16,15 +15,20 @@ import { fromError } from "zod-validation-error";
import { sendEmail } from "@server/emails"; import { sendEmail } from "@server/emails";
import SendInviteLink from "@server/emails/templates/SendInviteLink"; import SendInviteLink from "@server/emails/templates/SendInviteLink";
const inviteUserParamsSchema = z.object({ const inviteUserParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const inviteUserBodySchema = z.object({ const inviteUserBodySchema = z
.object({
email: z.string().email(), email: z.string().email(),
roleId: z.number(), roleId: z.number(),
validHours: z.number().gt(0).lte(168), validHours: z.number().gt(0).lte(168),
}); sendEmail: z.boolean().optional()
})
.strict();
export type InviteUserBody = z.infer<typeof inviteUserBodySchema>; export type InviteUserBody = z.infer<typeof inviteUserBodySchema>;
@ -38,7 +42,7 @@ const inviteTracker: Record<string, { timestamps: number[] }> = {};
export async function inviteUser( export async function inviteUser(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedParams = inviteUserParamsSchema.safeParse(req.params); const parsedParams = inviteUserParamsSchema.safeParse(req.params);
@ -46,8 +50,8 @@ export async function inviteUser(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString(), fromError(parsedParams.error).toString()
), )
); );
} }
@ -56,13 +60,18 @@ export async function inviteUser(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString(), fromError(parsedBody.error).toString()
), )
); );
} }
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
const { email, validHours, roleId } = parsedBody.data; const {
email,
validHours,
roleId,
sendEmail: doEmail
} = parsedBody.data;
const currentTime = Date.now(); const currentTime = Date.now();
const oneHourAgo = currentTime - 3600000; const oneHourAgo = currentTime - 3600000;
@ -79,8 +88,8 @@ export async function inviteUser(
return next( return next(
createHttpError( createHttpError(
HttpCode.TOO_MANY_REQUESTS, 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); .limit(1);
if (!org.length) { if (!org.length) {
return next( 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"User is already a member of this organization", "User is already a member of this organization"
), )
); );
} }
const inviteId = generateRandomString( const inviteId = generateRandomString(
10, 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 token = generateRandomString(32, alphabet("a-z", "A-Z", "0-9"));
const expiresAt = createDate(new TimeSpan(validHours, "h")).getTime(); const expiresAt = createDate(new TimeSpan(validHours, "h")).getTime();
@ -125,7 +134,7 @@ export async function inviteUser(
await db await db
.delete(userInvites) .delete(userInvites)
.where( .where(
and(eq(userInvites.email, email), eq(userInvites.orgId, orgId)), and(eq(userInvites.email, email), eq(userInvites.orgId, orgId))
) )
.execute(); .execute();
@ -135,43 +144,42 @@ export async function inviteUser(
email, email,
expiresAt, expiresAt,
tokenHash, tokenHash,
roleId, roleId
}); });
const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`; const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`;
if (doEmail) {
await sendEmail( await sendEmail(
SendInviteLink({ SendInviteLink({
email, email,
inviteLink, inviteLink,
expiresInDays: (validHours / 24).toString(), expiresInDays: (validHours / 24).toString(),
orgName: org[0].name || orgId, orgName: org[0].name || orgId,
inviterName: req.user?.email, inviterName: req.user?.email
}), }),
{ {
to: email, to: email,
from: config.email?.no_reply, from: config.email?.no_reply,
subject: "You're invited to join a Fossorial organization", subject: "You're invited to join a Fossorial organization"
}, }
); );
}
return response<InviteUserResponse>(res, { return response<InviteUserResponse>(res, {
data: { data: {
inviteLink, inviteLink,
expiresAt, expiresAt
}, },
success: true, success: true,
error: false, error: false,
message: "User invited successfully", message: "User invited successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
console.error(error); logger.error(error);
return next( return next(
createHttpError( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred",
),
); );
} }
} }

View file

@ -8,11 +8,14 @@ import createHttpError from "http-errors";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
const listUsersParamsSchema = z.object({ const listUsersParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const listUsersSchema = z.object({ const listUsersSchema = z
.object({
limit: z limit: z
.string() .string()
.optional() .optional()
@ -24,8 +27,9 @@ const listUsersSchema = z.object({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.number().int().nonnegative()), .pipe(z.number().int().nonnegative())
}); })
.strict();
async function queryUsers(orgId: string, limit: number, offset: number) { async function queryUsers(orgId: string, limit: number, offset: number) {
return await db return await db
@ -37,7 +41,7 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
orgId: userOrgs.orgId, orgId: userOrgs.orgId,
roleId: userOrgs.roleId, roleId: userOrgs.roleId,
roleName: roles.name, roleName: roles.name,
isOwner: userOrgs.isOwner, isOwner: userOrgs.isOwner
}) })
.from(users) .from(users)
.leftJoin(userOrgs, sql`${users.userId} = ${userOrgs.userId}`) .leftJoin(userOrgs, sql`${users.userId} = ${userOrgs.userId}`)
@ -97,13 +101,13 @@ export async function listUsers(
pagination: { pagination: {
total: count, total: count,
limit, limit,
offset, offset
}, }
}, },
success: true, success: true,
error: false, error: false,
message: "Users retrieved successfully", message: "Users retrieved successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,14 +9,18 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const removeUserActionParamsSchema = z.object({ const removeUserActionParamsSchema = z
userId: z.string(), .object({
}); userId: z.string()
})
.strict();
const removeUserActionSchema = z.object({ const removeUserActionSchema = z
.object({
actionId: z.string(), actionId: z.string(),
orgId: z.string(), orgId: z.string()
}); })
.strict();
export async function removeUserAction( export async function removeUserAction(
req: Request, req: Request,
@ -73,7 +77,7 @@ export async function removeUserAction(
success: true, success: true,
error: false, error: false,
message: "Action removed from user successfully", message: "Action removed from user successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,10 +9,12 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const removeUserSchema = z.object({ const removeUserSchema = z
.object({
userId: z.string(), userId: z.string(),
orgId: z.string(), orgId: z.string()
}); })
.strict();
export async function removeUserOrg( export async function removeUserOrg(
req: Request, req: Request,
@ -70,7 +72,7 @@ export async function removeUserOrg(
success: true, success: true,
error: false, error: false,
message: "User remove from org successfully", message: "User remove from org successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,10 +9,15 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const removeUserResourceSchema = z.object({ const removeUserResourceSchema = z
.object({
userId: z.string(), userId: z.string(),
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z
}); .string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
export async function removeUserResource( export async function removeUserResource(
req: Request, req: Request,
@ -56,7 +61,7 @@ export async function removeUserResource(
success: true, success: true,
error: false, error: false,
message: "Resource removed from user successfully", message: "Resource removed from user successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -9,13 +9,17 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const removeUserSiteParamsSchema = z.object({ const removeUserSiteParamsSchema = z
userId: z.string(), .object({
}); userId: z.string()
})
.strict();
const removeUserSiteSchema = z.object({ const removeUserSiteSchema = z
siteId: z.number().int().positive(), .object({
}); siteId: z.number().int().positive()
})
.strict();
export async function removeUserSite( export async function removeUserSite(
req: Request, req: Request,
@ -85,7 +89,7 @@ export async function removeUserSite(
success: true, success: true,
error: false, error: false,
message: "Site removed from user successfully", message: "Site removed from user successfully",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View file

@ -126,7 +126,7 @@ export default function CreateRoleForm({
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8:w" className="space-y-8"
id="create-role-form" id="create-role-form"
> >
<FormField <FormField

View file

@ -5,7 +5,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
@ -54,11 +54,11 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "description", accessorKey: "description",
header: "Description", header: "Description"
}, },
{ {
id: "actions", id: "actions",
@ -90,16 +90,15 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem> <DropdownMenuItem
<button
className="text-red-500"
onClick={() => { onClick={() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
setUserToRemove(roleRow); setUserToRemove(roleRow);
}} }}
> >
<span className="text-red-500">
Delete Role Delete Role
</button> </span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -107,8 +106,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
</div> </div>
</> </>
); );
}, }
}, }
]; ];
return ( return (
@ -128,9 +127,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
roleToDelete={roleToRemove} roleToDelete={roleToRemove}
afterDelete={() => { afterDelete={() => {
setRoles((prev) => setRoles((prev) =>
prev.filter( prev.filter((r) => r.roleId !== roleToRemove.roleId)
(r) => r.roleId !== roleToRemove.roleId,
),
); );
setUserToRemove(null); setUserToRemove(null);
}} }}

View file

@ -7,7 +7,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { import {
@ -15,7 +15,7 @@ import {
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -33,13 +33,14 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api"; import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Checkbox } from "@app/components/ui/checkbox";
type InviteUserFormProps = { type InviteUserFormProps = {
open: boolean; open: boolean;
@ -49,14 +50,16 @@ type InviteUserFormProps = {
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email({ message: "Invalid email address" }), email: z.string().email({ message: "Invalid email address" }),
validForHours: z.string().min(1, { message: "Please select a duration" }), 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) { export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
const { toast } = useToast(); const { toast } = useToast();
const { org } = useOrgContext(); const { org } = useOrgContext();
const api = createApiClient(useEnvContext()); const { env } = useEnvContext();
const api = createApiClient({ env });
const [inviteLink, setInviteLink] = useState<string | null>(null); const [inviteLink, setInviteLink] = useState<string | null>(null);
const [loading, setLoading] = useState(false); 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 [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
const [sendEmail, setSendEmail] = useState(env.EMAIL_ENABLED === "true");
const validFor = [ const validFor = [
{ hours: 24, name: "1 day" }, { hours: 24, name: "1 day" },
{ hours: 48, name: "2 days" }, { hours: 48, name: "2 days" },
@ -71,7 +76,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
{ hours: 96, name: "4 days" }, { hours: 96, name: "4 days" },
{ hours: 120, name: "5 days" }, { hours: 120, name: "5 days" },
{ hours: 144, name: "6 days" }, { hours: 144, name: "6 days" },
{ hours: 168, name: "7 days" }, { hours: 168, name: "7 days" }
]; ];
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -79,8 +84,8 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
defaultValues: { defaultValues: {
email: "", email: "",
validForHours: "72", validForHours: "72",
roleId: "", roleId: ""
}, }
}); });
useEffect(() => { useEffect(() => {
@ -90,9 +95,9 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
async function fetchRoles() { async function fetchRoles() {
const res = await api const res = await api
.get<AxiosResponse<ListRolesResponse>>( .get<
`/org/${org?.org.orgId}/roles` AxiosResponse<ListRolesResponse>
) >(`/org/${org?.org.orgId}/roles`)
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
toast({ toast({
@ -101,7 +106,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while fetching the roles" "An error occurred while fetching the roles"
), )
}); });
}); });
@ -127,6 +132,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
email: values.email, email: values.email,
roleId: parseInt(values.roleId), roleId: parseInt(values.roleId),
validHours: parseInt(values.validForHours), validHours: parseInt(values.validForHours),
sendEmail: sendEmail
} as InviteUserBody } as InviteUserBody
) )
.catch((e) => { .catch((e) => {
@ -136,7 +142,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while inviting the user" "An error occurred while inviting the user"
), )
}); });
}); });
@ -145,7 +151,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
toast({ toast({
variant: "default", variant: "default",
title: "User invited", title: "User invited",
description: "The user has been successfully invited.", description: "The user has been successfully invited."
}); });
setExpiresInDays(parseInt(values.validForHours) / 24); setExpiresInDays(parseInt(values.validForHours) / 24);
@ -198,6 +204,27 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
</FormItem> </FormItem>
)} )}
/> />
{env.EMAIL_ENABLED === "true" && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(e) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Send invite email to user
</label>
</div>
)}
<FormField <FormField
control={form.control} control={form.control}
name="roleId" name="roleId"
@ -281,11 +308,21 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
{inviteLink && ( {inviteLink && (
<div className="max-w-md space-y-4"> <div className="max-w-md space-y-4">
{sendEmail && (
<p> <p>
The user has been successfully invited. An email has been sent to the user
They must access the link below to with the access link below. They
accept the invitation. must access the link to accept the
invitation.
</p> </p>
)}
{!sendEmail && (
<p>
The user has been invited. They must
access the link below to accept the
invitation.
</p>
)}
<p> <p>
The invite will expire in{" "} The invite will expire in{" "}
<b> <b>

View file

@ -5,7 +5,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
@ -64,7 +64,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "status", accessorKey: "status",
@ -80,7 +80,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "role", accessorKey: "role",
@ -108,7 +108,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
<span>{userRow.role}</span> <span>{userRow.role}</span>
</div> </div>
); );
}, }
}, },
{ {
id: "actions", id: "actions",
@ -149,20 +149,19 @@ export default function UsersTable({ users: u }: UsersTableProps) {
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
{userRow.email !== user?.email && ( {userRow.email !== user?.email && (
<DropdownMenuItem> <DropdownMenuItem
<button
className="text-red-500"
onClick={() => { onClick={() => {
setIsDeleteModalOpen( setIsDeleteModalOpen(
true, true
); );
setSelectedUser( setSelectedUser(
userRow, userRow
); );
}} }}
> >
<span className="text-red-500">
Remove User Remove User
</button> </span>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> </DropdownMenuContent>
@ -183,8 +182,8 @@ export default function UsersTable({ users: u }: UsersTableProps) {
</div> </div>
</> </>
); );
}, }
}, }
]; ];
async function removeUser() { async function removeUser() {
@ -197,8 +196,8 @@ export default function UsersTable({ users: u }: UsersTableProps) {
title: "Failed to remove user", title: "Failed to remove user",
description: formatAxiosError( description: formatAxiosError(
e, 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({ toast({
variant: "default", variant: "default",
title: "User removed", 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) => setUsers((prev) =>
prev.filter((u) => u.id !== selectedUser?.id), prev.filter((u) => u.id !== selectedUser?.id)
); );
} }
} }

View file

@ -637,6 +637,7 @@ export default function ResourceAuthenticationPage() {
</span> </span>
</div> </div>
{whitelistEnabled && (
<Form {...whitelistForm}> <Form {...whitelistForm}>
<form className="space-y-8"> <form className="space-y-8">
<FormField <FormField
@ -653,12 +654,15 @@ export default function ResourceAuthenticationPage() {
activeTagIndex={ activeTagIndex={
activeEmailTagIndex activeEmailTagIndex
} }
validateTag={(tag) => { validateTag={(
tag
) => {
return z return z
.string() .string()
.email() .email()
.safeParse(tag) .safeParse(
.success; tag
).success;
}} }}
setActiveTagIndex={ setActiveTagIndex={
setActiveEmailTagIndex setActiveEmailTagIndex
@ -668,7 +672,9 @@ export default function ResourceAuthenticationPage() {
whitelistForm.getValues() whitelistForm.getValues()
.emails .emails
} }
setTags={(newRoles) => { setTags={(
newRoles
) => {
whitelistForm.setValue( whitelistForm.setValue(
"emails", "emails",
newRoles as [ newRoles as [
@ -677,7 +683,9 @@ export default function ResourceAuthenticationPage() {
] ]
); );
}} }}
allowDuplicates={false} allowDuplicates={
false
}
sortTags={true} sortTags={true}
styleClasses={{ styleClasses={{
tag: { tag: {
@ -694,6 +702,7 @@ export default function ResourceAuthenticationPage() {
/> />
</form> </form>
</Form> </Form>
)}
<Button <Button
loading={loadingSaveWhitelist} loading={loadingSaveWhitelist}

View file

@ -6,7 +6,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
@ -17,7 +17,7 @@ import {
Check, Check,
ArrowUpRight, ArrowUpRight,
ShieldOff, ShieldOff,
ShieldCheck, ShieldCheck
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@ -64,7 +64,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error deleting resource", title: "Error deleting resource",
description: formatAxiosError(e, "Error deleting resource"), description: formatAxiosError(e, "Error deleting resource")
}); });
}) })
.then(() => { .then(() => {
@ -88,7 +88,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "site", accessorKey: "site",
@ -117,7 +117,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
<ArrowUpRight className="ml-2 h-4 w-4" /> <ArrowUpRight className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "domain", accessorKey: "domain",
@ -139,16 +139,16 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
className="h-6 w-6 p-0" className="h-6 w-6 p-0"
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
resourceRow.domain, resourceRow.domain
); );
const originalIcon = document.querySelector( const originalIcon = document.querySelector(
`#icon-${resourceRow.id}`, `#icon-${resourceRow.id}`
); );
if (originalIcon) { if (originalIcon) {
originalIcon.classList.add("hidden"); originalIcon.classList.add("hidden");
} }
const checkIcon = document.querySelector( const checkIcon = document.querySelector(
`#check-icon-${resourceRow.id}`, `#check-icon-${resourceRow.id}`
); );
if (checkIcon) { if (checkIcon) {
checkIcon.classList.remove("hidden"); checkIcon.classList.remove("hidden");
@ -156,7 +156,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
checkIcon.classList.add("hidden"); checkIcon.classList.add("hidden");
if (originalIcon) { if (originalIcon) {
originalIcon.classList.remove( originalIcon.classList.remove(
"hidden", "hidden"
); );
} }
}, 2000); }, 2000);
@ -175,7 +175,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
</Button> </Button>
</div> </div>
); );
}, }
}, },
{ {
accessorKey: "hasAuth", accessorKey: "hasAuth",
@ -209,7 +209,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
)} )}
</div> </div>
); );
}, }
}, },
{ {
id: "actions", id: "actions",
@ -241,18 +241,15 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
View settings View settings
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem
<button
onClick={() => { onClick={() => {
setSelectedResource( setSelectedResource(resourceRow);
resourceRow,
);
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}} }}
className="text-red-500"
> >
<span className="text-red-500">
Delete Delete
</button> </span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -267,8 +264,8 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
</div> </div>
</> </>
); );
}, }
}, }
]; ];
return ( return (

View file

@ -197,12 +197,16 @@ export default function CreateShareLinkForm({
const link = constructShareLink( const link = constructShareLink(
values.resourceId, values.resourceId,
token.accessTokenId, token.accessTokenId,
token.tokenHash token.accessToken
); );
setLink(link); setLink(link);
onCreated?.({ onCreated?.({
...token, accessTokenId: token.accessTokenId,
resourceName: values.resourceName resourceId: token.resourceId,
resourceName: values.resourceName,
title: token.title,
createdAt: token.createdAt,
expiresAt: token.expiresAt
}); });
} }
@ -285,7 +289,9 @@ export default function CreateShareLinkForm({
r r
) => ( ) => (
<CommandItem <CommandItem
value={r.name} value={
r.name
}
key={ key={
r.resourceId r.resourceId
} }
@ -441,6 +447,10 @@ export default function CreateShareLinkForm({
)} )}
{link && ( {link && (
<div className="max-w-md space-y-4"> <div className="max-w-md space-y-4">
<p>
You will be able to see this link once.
Make sure to copy it.
</p>
<p> <p>
Anyone with this link can access the Anyone with this link can access the
resource. Share it with care. resource. Share it with care.

View file

@ -34,9 +34,14 @@ import moment from "moment";
import CreateShareLinkForm from "./CreateShareLinkForm"; import CreateShareLinkForm from "./CreateShareLinkForm";
import { constructShareLink } from "@app/lib/shareLinks"; import { constructShareLink } from "@app/lib/shareLinks";
export type ShareLinkRow = ArrayElement< export type ShareLinkRow = {
ListAccessTokensResponse["accessTokens"] accessTokenId: string;
>; resourceId: number;
resourceName: string;
title: string | null;
createdAt: number;
expiresAt: number | null;
};
type ShareLinksTableProps = { type ShareLinksTableProps = {
shareLinks: ShareLinkRow[]; shareLinks: ShareLinkRow[];
@ -64,7 +69,10 @@ export default function ShareLinksTable({
await api.delete(`/access-token/${id}`).catch((e) => { await api.delete(`/access-token/${id}`).catch((e) => {
toast({ toast({
title: "Failed to delete link", 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({ toast({
title: "Link deleted", 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", // accessorKey: "domain",
header: "Link", // header: "Link",
cell: ({ row }) => { // cell: ({ row }) => {
const r = row.original; // const r = row.original;
//
const link = constructShareLink( // const link = constructShareLink(
r.resourceId, // r.resourceId,
r.accessTokenId, // r.accessTokenId,
r.tokenHash // r.tokenHash
); // );
//
return ( // return (
<div className="flex items-center"> // <div className="flex items-center">
<Link // <Link
href={link} // href={link}
target="_blank" // target="_blank"
rel="noopener noreferrer" // rel="noopener noreferrer"
className="hover:underline mr-2" // className="hover:underline mr-2"
> // >
{formatLink(link)} // {formatLink(link)}
</Link> // </Link>
<Button // <Button
variant="ghost" // variant="ghost"
className="h-6 w-6 p-0" // className="h-6 w-6 p-0"
onClick={() => { // onClick={() => {
navigator.clipboard.writeText(link); // navigator.clipboard.writeText(link);
const originalIcon = document.querySelector( // const originalIcon = document.querySelector(
`#icon-${r.accessTokenId}` // `#icon-${r.accessTokenId}`
); // );
if (originalIcon) { // if (originalIcon) {
originalIcon.classList.add("hidden"); // originalIcon.classList.add("hidden");
} // }
const checkIcon = document.querySelector( // const checkIcon = document.querySelector(
`#check-icon-${r.accessTokenId}` // `#check-icon-${r.accessTokenId}`
); // );
if (checkIcon) { // if (checkIcon) {
checkIcon.classList.remove("hidden"); // checkIcon.classList.remove("hidden");
setTimeout(() => { // setTimeout(() => {
checkIcon.classList.add("hidden"); // checkIcon.classList.add("hidden");
if (originalIcon) { // if (originalIcon) {
originalIcon.classList.remove( // originalIcon.classList.remove(
"hidden" // "hidden"
); // );
} // }
}, 2000); // }, 2000);
} // }
}} // }}
> // >
<Copy // <Copy
id={`icon-${r.accessTokenId}`} // id={`icon-${r.accessTokenId}`}
className="h-4 w-4" // className="h-4 w-4"
/> // />
<Check // <Check
id={`check-icon-${r.accessTokenId}`} // id={`check-icon-${r.accessTokenId}`}
className="hidden text-green-500 h-4 w-4" // className="hidden text-green-500 h-4 w-4"
/> // />
<span className="sr-only">Copy link</span> // <span className="sr-only">Copy link</span>
</Button> // </Button>
</div> // </div>
); // );
} // }
}, // },
{ {
accessorKey: "createdAt", accessorKey: "createdAt",
header: ({ column }) => { header: ({ column }) => {

View file

@ -46,9 +46,9 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) {
redirect(`/${params.orgId}/settings/resources`); redirect(`/${params.orgId}/settings/resources`);
} }
const rows: ShareLinkRow[] = tokens.map((token) => { const rows: ShareLinkRow[] = tokens.map(
return token; (token) => ({ ...token }) as ShareLinkRow
}); );
return ( return (
<> <>

View file

@ -8,7 +8,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
@ -24,11 +24,15 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { useParams, useRouter } from "next/navigation"; 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 { generateKeypair } from "../[niceId]/components/wireguardConfig";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import { Checkbox } from "@app/components/ui/checkbox"; import { Checkbox } from "@app/components/ui/checkbox";
@ -37,42 +41,49 @@ import {
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api"; import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { SiteRow } from "./SitesTable";
import { AxiosResponse } from "axios";
const method = [ const method = [
{ label: "Newt", value: "newt" }, { label: "Newt", value: "newt" },
{ label: "Wireguard", value: "wireguard" }, { label: "WireGuard", value: "wireguard" }
] as const; ] as const;
const accountFormSchema = z.object({ const createSiteFormSchema = z.object({
name: z name: z
.string() .string()
.min(2, { .min(2, {
message: "Name must be at least 2 characters.", message: "Name must be at least 2 characters."
}) })
.max(30, { .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<typeof accountFormSchema>; type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
const defaultValues: Partial<AccountFormValues> = { const defaultValues: Partial<CreateSiteFormValues> = {
name: "", name: "",
method: "newt", method: "newt"
}; };
type CreateSiteFormProps = { type CreateSiteFormProps = {
open: boolean; open: boolean;
setOpen: (open: boolean) => void; 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 { toast } = useToast();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
@ -96,9 +107,9 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
setIsChecked(checked); setIsChecked(checked);
}; };
const form = useForm<AccountFormValues>({ const form = useForm<CreateSiteFormValues>({
resolver: zodResolver(accountFormSchema), resolver: zodResolver(createSiteFormSchema),
defaultValues, defaultValues
}); });
useEffect(() => { useEffect(() => {
@ -114,7 +125,7 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error picking site defaults", title: "Error picking site defaults",
description: formatAxiosError(e), description: formatAxiosError(e)
}); });
}) })
.then((res) => { .then((res) => {
@ -125,7 +136,7 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
} }
}, [open]); }, [open]);
async function onSubmit(data: AccountFormValues) { async function onSubmit(data: CreateSiteFormValues) {
setLoading(true); setLoading(true);
if (!siteDefaults || !keypair) { if (!siteDefaults || !keypair) {
return; return;
@ -135,29 +146,44 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
subnet: siteDefaults.subnet, subnet: siteDefaults.subnet,
exitNodeId: siteDefaults.exitNodeId, exitNodeId: siteDefaults.exitNodeId,
pubKey: keypair.publicKey, pubKey: keypair.publicKey,
type: data.method, type: data.method
}; };
if (data.method === "newt") { if (data.method === "newt") {
payload.secret = siteDefaults.newtSecret; payload.secret = siteDefaults.newtSecret;
payload.newtId = siteDefaults.newtId; payload.newtId = siteDefaults.newtId;
} }
const res = await api const res = await api
.put(`/org/${orgId}/site/`, payload) .put<
AxiosResponse<CreateSiteResponse>
>(`/org/${orgId}/site/`, payload)
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error creating site", title: "Error creating site",
description: formatAxiosError(e), description: formatAxiosError(e)
}); });
}); });
if (res && res.status === 201) { if (res && res.status === 201) {
const niceId = res.data.data.niceId; const niceId = res.data.data.niceId;
// navigate to the site page // navigate to the site page
router.push(`/${orgId}/settings/sites/${niceId}`); // router.push(`/${orgId}/settings/sites/${niceId}`);
// close the modal // close the modal
setOpen(false); 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); setLoading(false);
@ -275,8 +301,8 @@ PersistentKeepalive = 5`
{form.watch("method") === "wireguard" && {form.watch("method") === "wireguard" &&
!isLoading ? ( !isLoading ? (
<CopyTextBox text={wgConfig} /> <CopyTextBox text={wgConfig} />
) : form.watch("method") === "wireguard" && ) : form.watch("method") ===
isLoading ? ( "wireguard" && isLoading ? (
<p> <p>
Loading WireGuard Loading WireGuard
configuration... configuration...

View file

@ -6,10 +6,16 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; 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 Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
@ -45,14 +51,10 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null); const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
const [rows, setRows] = useState<SiteRow[]>(sites);
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const callApi = async () => {
const res = await api.put<AxiosResponse<any>>(`/newt`);
console.log(res);
};
const deleteSite = (siteId: number) => { const deleteSite = (siteId: number) => {
api.delete(`/site/${siteId}`) api.delete(`/site/${siteId}`)
.catch((e) => { .catch((e) => {
@ -60,7 +62,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error deleting site", title: "Error deleting site",
description: formatAxiosError(e, "Error deleting site"), description: formatAxiosError(e, "Error deleting site")
}); });
}) })
.then(() => { .then(() => {
@ -84,7 +86,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "nice", accessorKey: "nice",
@ -100,7 +102,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "mbIn", accessorKey: "mbIn",
@ -116,7 +118,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "mbOut", accessorKey: "mbOut",
@ -132,7 +134,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, }
}, },
{ {
accessorKey: "type", accessorKey: "type",
@ -167,7 +169,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</div> </div>
); );
} }
}, }
}, },
{ {
accessorKey: "online", accessorKey: "online",
@ -203,7 +205,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</span> </span>
); );
} }
}, }
}, },
{ {
id: "actions", id: "actions",
@ -229,16 +231,13 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
View settings View settings
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem
<button
onClick={() => { onClick={() => {
setSelectedSite(siteRow); setSelectedSite(siteRow);
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}} }}
className="text-red-500"
> >
Delete <span className="text-red-500">Delete</span>
</button>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -252,8 +251,8 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</Link> </Link>
</div> </div>
); );
}, }
}, }
]; ];
return ( return (
@ -261,6 +260,9 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<CreateSiteForm <CreateSiteForm
open={isCreateModalOpen} open={isCreateModalOpen}
setOpen={setIsCreateModalOpen} setOpen={setIsCreateModalOpen}
onCreate={(val) => {
setRows([val, ...rows]);
}}
/> />
{selectedSite && ( {selectedSite && (
@ -302,12 +304,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<SitesDataTable <SitesDataTable
columns={columns} columns={columns}
data={sites} data={rows}
addSite={() => { addSite={() => {
setIsCreateModalOpen(true); setIsCreateModalOpen(true);
}} }}
/> />
{/* <button onClick={callApi}>Create Newt</button> */}
</> </>
); );
} }

View file

@ -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<AuthWithAccessTokenResponse>
>(`/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 ? (
<div></div>
) : (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-center text-2xl font-bold">
Access URL Invalid
</CardTitle>
</CardHeader>
<CardContent>
This shared access URL is invalid. Please contact the resource
owner for a new URL.
<div className="text-center mt-4">
<Button>
<Link href="/">Go Home</Link>
</Button>
</div>
</CardContent>
</Card>
);
}

View file

@ -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 (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-center text-2xl font-bold">
Acess URL Invalid
</CardTitle>
</CardHeader>
<CardContent>
This shared access URL is invalid. Please contact the resource
owner for a new URL.
<div className="text-center mt-4">
<Button>
<Link href="/">Go Home</Link>
</Button>
</div>
</CardContent>
</Card>
);
}

View file

@ -14,7 +14,8 @@ import ResourceNotFound from "./components/ResourceNotFound";
import ResourceAccessDenied from "./components/ResourceAccessDenied"; import ResourceAccessDenied from "./components/ResourceAccessDenied";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { CheckResourceSessionResponse } from "@server/routers/auth"; 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: { export default async function ResourceAuthPage(props: {
params: Promise<{ resourceId: number }>; params: Promise<{ resourceId: number }>;
@ -50,35 +51,6 @@ export default async function ResourceAuthPage(props: {
const redirectUrl = searchParams.redirect || authInfo.url; const redirectUrl = searchParams.redirect || authInfo.url;
if (searchParams.token) {
let doRedirect = false;
try {
const res = await internal.post<
AxiosResponse<AuthWithAccessTokenResponse>
>(
`/auth/resource/${params.resourceId}/access-token`,
{
accessToken: searchParams.token
},
await authCookieHeader()
);
if (res.data.data.session) {
doRedirect = true;
}
} catch (e) {
return (
<div className="w-full max-w-md">
<AccessTokenInvalid />
</div>
);
}
if (doRedirect) {
redirect(redirectUrl);
}
}
const hasAuth = const hasAuth =
authInfo.password || authInfo.password ||
authInfo.pincode || authInfo.pincode ||
@ -146,6 +118,20 @@ export default async function ResourceAuthPage(props: {
} }
} }
if (searchParams.token) {
const [accessTokenId, accessToken] = searchParams.token.split(".");
return (
<div className="w-full max-w-md">
<AccessToken
accessToken={accessToken}
accessTokenId={accessTokenId}
resourceId={params.resourceId}
redirectUrl={redirectUrl}
/>
</div>
);
}
return ( return (
<> <>
{userIsUnauthorized && isSSOOnly ? ( {userIsUnauthorized && isSSOOnly ? (