This commit is contained in:
Owen Schwartz 2024-12-22 12:04:57 -05:00
commit 0386d81b95
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
95 changed files with 1640 additions and 1206 deletions

View file

@ -25,9 +25,10 @@ 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:
window_minutes: 1 global:
max_requests: 100 window_minutes: 1
max_requests: 100
email: email:
smtp_host: host.hoster.net smtp_host: host.hoster.net

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({
window_minutes: z.number().positive().gt(0), global: z.object({
max_requests: z.number().positive().gt(0), window_minutes: z.number().positive().gt(0),
max_requests: z.number().positive().gt(0)
}),
auth: z
.object({
window_minutes: z.number().positive().gt(0),
max_requests: z.number().positive().gt(0)
})
.optional()
}), }),
email: z 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

@ -96,7 +96,9 @@ export const newts = sqliteTable("newt", {
newtId: text("id").primaryKey(), newtId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(), secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(), dateCreated: text("dateCreated").notNull(),
siteId: integer("siteId").references(() => sites.siteId) siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
})
}); });
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {

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
accessTokenId: z.string() .object({
}); 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
validForSeconds: z.number().int().positive().optional(), // seconds .object({
title: z.string().optional(), validForSeconds: z.number().int().positive().optional(), // seconds
description: z.string().optional() title: 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
accessTokenId: id, .insert(resourceAccessToken)
orgId: resource.orgId, .values({
resourceId, accessTokenId: id,
tokenHash: token, orgId: resource.orgId,
expiresAt: expiresAt || null, resourceId,
sessionLength: sessionLength, tokenHash,
title: title || null, expiresAt: expiresAt || null,
description: description || null, sessionLength: sessionLength,
createdAt: new Date().getTime() title: title || null,
}).returning(); description: description || null,
createdAt: new Date().getTime()
})
.returning({
accessTokenId: resourceAccessToken.accessTokenId,
orgId: resourceAccessToken.orgId,
resourceId: resourceAccessToken.resourceId,
expiresAt: resourceAccessToken.expiresAt,
sessionLength: resourceAccessToken.sessionLength,
title: resourceAccessToken.title,
description: resourceAccessToken.description,
createdAt: resourceAccessToken.createdAt
})
.execute();
if (!result) { 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>;
@ -149,6 +149,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>;
@ -85,6 +85,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
token: z.string(), .object({
newPassword: passwordSchema, token: z.string(),
code: z.string().optional(), newPassword: passwordSchema,
}); 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) {
return allowed(res); if (pincode && resourceSession.pincodeId) {
logger.debug(
"Resource allowed because pincode session is valid"
);
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

@ -18,9 +18,11 @@ import { fromError } from "zod-validation-error";
import { sendToClient } from "../ws"; import { sendToClient } from "../ws";
import { deletePeer } from "../gerbil/peers"; import { deletePeer } from "../gerbil/peers";
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,

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,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 updateOrgParamsSchema = z.object({ const updateOrgParamsSchema = z
orgId: z.string(), .object({
}); orgId: z.string()
})
.strict();
const updateOrgBodySchema = z const updateOrgBodySchema = z
.object({ .object({
@ -20,7 +22,7 @@ const updateOrgBodySchema = 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 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,14 +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
// }); });
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
password: z.string() .object({
}); 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
pincode: z.string() .object({
}); 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
email: z.string().email(), .object({
otp: z.string().optional() email: z.string().email(),
}); 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
siteId: z.string().transform(stoi).pipe(z.number().int().positive()), .object({
orgId: z.string(), siteId: z.string().transform(stoi).pipe(z.number().int().positive()),
}); 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
emails: z.array(z.string().email()).max(50) .object({
}); 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,16 +10,18 @@ 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
siteId: z .object({
.string() siteId: z
.optional() .string()
.transform(stoi) .optional()
.pipe(z.number().int().positive().optional()) .transform(stoi)
.optional(), .pipe(z.number().int().positive().optional())
niceId: z.string().optional(), .optional(),
orgId: z.string().optional(), niceId: 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
token: z.string(), .object({
inviteId: z.string(), token: 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
userId: z.string(), .object({
actionId: z.string(), userId: z.string(),
orgId: z.string(), actionId: 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
userId: z.string(), .object({
roleId: z.string().transform(stoi).pipe(z.number()), userId: z.string(),
}); 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
userId: z.string(), .object({
siteId: z.string().transform(Number).pipe(z.number().int().positive()), userId: z.string(),
}); 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
userId: z.string(), .object({
orgId: z.string(), userId: 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
email: z.string().email(), .object({
roleId: z.number(), email: z.string().email(),
validHours: z.number().gt(0).lte(168), roleId: z.number(),
}); 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}`;
await sendEmail( if (doEmail) {
SendInviteLink({ await sendEmail(
email, SendInviteLink({
inviteLink, email,
expiresInDays: (validHours / 24).toString(), inviteLink,
orgName: org[0].name || orgId, expiresInDays: (validHours / 24).toString(),
inviterName: req.user?.email, orgName: org[0].name || orgId,
}), inviterName: req.user?.email
{ }),
to: email, {
from: config.email?.no_reply, to: email,
subject: "You're invited to join a Fossorial organization", from: config.email?.no_reply,
}, 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,24 +8,28 @@ 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
limit: z .object({
.string() limit: z
.optional() .string()
.default("1000") .optional()
.transform(Number) .default("1000")
.pipe(z.number().int().nonnegative()), .transform(Number)
offset: z .pipe(z.number().int().nonnegative()),
.string() offset: z
.optional() .string()
.default("0") .optional()
.transform(Number) .default("0")
.pipe(z.number().int().nonnegative()), .transform(Number)
}); .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
actionId: z.string(), .object({
orgId: z.string(), actionId: 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
userId: z.string(), .object({
orgId: z.string(), userId: 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
userId: z.string(), .object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), userId: z.string(),
}); 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 onClick={() => {
className="text-red-500" setIsDeleteModalOpen(true);
onClick={() => { setUserToRemove(roleRow);
setIsDeleteModalOpen(true); }}
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">
<p> {sendEmail && (
The user has been successfully invited. <p>
They must access the link below to An email has been sent to the user
accept the invitation. with the access link below. They
</p> must access the link to accept the
invitation.
</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";
@ -43,7 +43,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const user = useUserContext(); const user = useUserContext();
const { org } = useOrgContext(); const { org } = useOrgContext();
@ -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 onClick={() => {
className="text-red-500" setIsDeleteModalOpen(
onClick={() => { true
setIsDeleteModalOpen( );
true, setSelectedUser(
); userRow
setSelectedUser( );
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,63 +637,72 @@ export default function ResourceAuthenticationPage() {
</span> </span>
</div> </div>
<Form {...whitelistForm}> {whitelistEnabled && (
<form className="space-y-8"> <Form {...whitelistForm}>
<FormField <form className="space-y-8">
control={whitelistForm.control} <FormField
name="emails" control={whitelistForm.control}
render={({ field }) => ( name="emails"
<FormItem className="flex flex-col items-start"> render={({ field }) => (
<FormLabel> <FormItem className="flex flex-col items-start">
Whitelisted Emails <FormLabel>
</FormLabel> Whitelisted Emails
<FormControl> </FormLabel>
<TagInput <FormControl>
{...field} <TagInput
activeTagIndex={ {...field}
activeEmailTagIndex activeTagIndex={
} activeEmailTagIndex
validateTag={(tag) => { }
return z validateTag={(
.string() tag
.email() ) => {
.safeParse(tag) return z
.success; .string()
}} .email()
setActiveTagIndex={ .safeParse(
setActiveEmailTagIndex tag
} ).success;
placeholder="Enter an email" }}
tags={ setActiveTagIndex={
whitelistForm.getValues() setActiveEmailTagIndex
.emails }
} placeholder="Enter an email"
setTags={(newRoles) => { tags={
whitelistForm.setValue( whitelistForm.getValues()
"emails", .emails
newRoles as [ }
Tag, setTags={(
...Tag[] newRoles
] ) => {
); whitelistForm.setValue(
}} "emails",
allowDuplicates={false} newRoles as [
sortTags={true} Tag,
styleClasses={{ ...Tag[]
tag: { ]
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full" );
}, }}
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none", allowDuplicates={
inlineTagsContainer: false
"bg-transparent" }
}} sortTags={true}
/> styleClasses={{
</FormControl> tag: {
</FormItem> body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
)} },
/> input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
</form> inlineTagsContainer:
</Form> "bg-transparent"
}}
/>
</FormControl>
</FormItem>
)}
/>
</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(resourceRow);
setSelectedResource( setIsDeleteModalOpen(true);
resourceRow, }}
); >
setIsDeleteModalOpen(true); <span className="text-red-500">
}}
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

@ -1,6 +1,5 @@
"use client"; "use client";
import { Button, buttonVariants } from "@app/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
@ -8,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 { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
@ -16,19 +15,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
} from "@app/components/Credenza";
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,96 +29,104 @@ 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 createSiteFormSchema = z.object({
{ label: "Newt", value: "newt" },
{ label: "Wireguard", value: "wireguard" },
] as const;
const accountFormSchema = 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; onCreate?: (site: SiteRow) => void;
setOpen: (open: boolean) => void; setLoading?: (loading: boolean) => void;
setChecked?: (checked: boolean) => void;
orgId: string;
}; };
export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { export default function CreateSiteForm({
onCreate,
setLoading,
setChecked,
orgId
}: CreateSiteFormProps) {
const { toast } = useToast(); const { toast } = useToast();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const [loading, setLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const params = useParams();
const orgId = params.orgId;
const router = useRouter(); const router = useRouter();
const [keypair, setKeypair] = useState<{ const [keypair, setKeypair] = useState<{
publicKey: string; publicKey: string;
privateKey: string; privateKey: string;
} | null>(null); } | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isChecked, setIsChecked] = useState(false);
const [siteDefaults, setSiteDefaults] = const [siteDefaults, setSiteDefaults] =
useState<PickSiteDefaultsResponse | null>(null); useState<PickSiteDefaultsResponse | null>(null);
const handleCheckboxChange = (checked: boolean) => { const handleCheckboxChange = (checked: boolean) => {
setChecked?.(checked);
setIsChecked(checked); setIsChecked(checked);
}; };
const form = useForm<AccountFormValues>({ const form = useForm<CreateSiteFormValues>({
resolver: zodResolver(accountFormSchema), resolver: zodResolver(createSiteFormSchema),
defaultValues, defaultValues
}); });
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
if (typeof window !== "undefined") { // reset all values
const generatedKeypair = generateKeypair(); setLoading?.(false);
setKeypair(generatedKeypair); setIsLoading(false);
setIsLoading(false); form.reset();
setChecked?.(false);
setKeypair(null);
setSiteDefaults(null);
api.get(`/org/${orgId}/pick-site-defaults`) const generatedKeypair = generateKeypair();
.catch((e) => { setKeypair(generatedKeypair);
toast({
variant: "destructive", api.get(`/org/${orgId}/pick-site-defaults`)
title: "Error picking site defaults", .catch((e) => {
description: formatAxiosError(e), toast({
}); variant: "destructive",
}) title: "Error picking site defaults",
.then((res) => { description: formatAxiosError(e)
if (res && res.status === 200) {
setSiteDefaults(res.data.data);
}
}); });
} })
.then((res) => {
if (res && res.status === 200) {
setSiteDefaults(res.data.data);
}
});
}, [open]); }, [open]);
async function onSubmit(data: AccountFormValues) { async function onSubmit(data: CreateSiteFormValues) {
setLoading(true); setLoading?.(true);
setIsLoading(true);
if (!siteDefaults || !keypair) { if (!siteDefaults || !keypair) {
return; return;
} }
@ -135,32 +135,45 @@ 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 const data = res.data.data;
setOpen(false);
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);
setIsLoading(false);
} }
const wgConfig = const wgConfig =
@ -186,148 +199,96 @@ PersistentKeepalive = 5`
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`; const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
return ( return (
<> <div className="space-y-8">
<Credenza <Form {...form}>
open={open} <form
onOpenChange={(val) => { onSubmit={form.handleSubmit(onSubmit)}
setOpen(val); className="space-y-8"
setLoading(false); id="create-site-form"
>
// reset all values <FormField
form.reset(); control={form.control}
setIsChecked(false); name="name"
setKeypair(null); render={({ field }) => (
setSiteDefaults(null); <FormItem>
}} <FormLabel>Name</FormLabel>
> <FormControl>
<CredenzaContent> <Input
<CredenzaHeader> autoComplete="off"
<CredenzaTitle>Create Site</CredenzaTitle> placeholder="Site name"
<CredenzaDescription> {...field}
Create a new site to start connecting your resources
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-8">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
id="create-site-form"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
autoComplete="off"
placeholder="Site name"
{...field}
/>
</FormControl>
<FormDescription>
This is the name that will
be displayed for this site.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={
field.onChange
}
>
<SelectTrigger>
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="wireguard">
WireGuard
</SelectItem>
<SelectItem value="newt">
Newt
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
This is how you will connect
your site to Fossorial.
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
</FormControl>
<FormDescription>
This is the name that will be displayed for
this site.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="wireguard">
WireGuard
</SelectItem>
<SelectItem value="newt">
Newt
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
This is how you will expose connections.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="max-w-md"> <div className="w-full">
{form.watch("method") === "wireguard" && {form.watch("method") === "wireguard" && !isLoading ? (
!isLoading ? ( <CopyTextBox text={wgConfig} />
<CopyTextBox text={wgConfig} /> ) : form.watch("method") === "wireguard" &&
) : form.watch("method") === "wireguard" && isLoading ? (
isLoading ? ( <p>Loading WireGuard configuration...</p>
<p> ) : (
Loading WireGuard <CopyTextBox text={newtConfig} wrapText={false} />
configuration... )}
</p> </div>
) : (
<CopyTextBox
text={newtConfig}
wrapText={false}
/>
)}
</div>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
You will only be able to see the You will only be able to see the configuration once.
configuration once. </span>
</span>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="terms" id="terms"
checked={isChecked} checked={isChecked}
onCheckedChange={ onCheckedChange={handleCheckboxChange}
handleCheckboxChange />
} <label
/> htmlFor="terms"
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I have copied the config
</label>
</div>
</form>
</Form>
</div>
</CredenzaBody>
<CredenzaFooter>
<Button
type="submit"
form="create-site-form"
loading={loading}
disabled={loading || !isChecked}
> >
Create Site I have copied the config
</Button> </label>
<CredenzaClose asChild> </div>
<Button variant="outline">Close</Button> </form>
</CredenzaClose> </Form>
</CredenzaFooter> </div>
</CredenzaContent>
</Credenza>
</>
); );
} }

View file

@ -0,0 +1,80 @@
"use client";
import { Button } from "@app/components/ui/button";
import { useState } from "react";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import { SiteRow } from "./SitesTable";
import CreateSiteForm from "./CreateSiteForm";
type CreateSiteFormProps = {
open: boolean;
setOpen: (open: boolean) => void;
onCreate?: (site: SiteRow) => void;
orgId: string;
};
export default function CreateSiteFormModal({
open,
setOpen,
onCreate,
orgId
}: CreateSiteFormProps) {
const [loading, setLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false);
return (
<>
<Credenza
open={open}
onOpenChange={(val) => {
setOpen(val);
setLoading(false);
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Create Site</CredenzaTitle>
<CredenzaDescription>
Create a new site to start connecting your resources
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="max-w-md">
<CreateSiteForm
setLoading={(val) => setLoading(val)}
setChecked={(val) => setIsChecked(val)}
onCreate={onCreate}
orgId={orgId}
/>
</div>
</CredenzaBody>
<CredenzaFooter>
<Button
type="submit"
form="create-site-form"
loading={loading}
disabled={loading || !isChecked}
onClick={() => {
setOpen(false);
}}
>
Create Site
</Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}

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";
@ -20,6 +26,7 @@ import { useToast } from "@app/hooks/useToast";
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 CreateSiteFormModal from "./CreateSiteModal";
export type SiteRow = { export type SiteRow = {
id: number; id: number;
@ -45,14 +52,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,12 +63,14 @@ 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(() => {
router.refresh(); router.refresh();
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
const newRows = rows.filter((row) => row.id !== siteId);
}); });
}; };
@ -84,7 +89,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 +105,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 +121,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 +137,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 +172,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</div> </div>
); );
} }
}, }
}, },
{ {
accessorKey: "online", accessorKey: "online",
@ -186,24 +191,23 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
}, },
cell: ({ row }) => { cell: ({ row }) => {
const originalRow = row.original; const originalRow = row.original;
console.log(originalRow.online);
if (originalRow.online) { if (originalRow.online) {
return ( return (
<span className="text-green-500 flex items-center space-x-2"> <span className="text-green-500 flex items-center space-x-2">
<Check className="w-4 h-4" /> <Check className="w-4 h-4" />
<span>Online</span> <span>Online</span>
</span> </span>
); );
} else { } else {
return ( return (
<span className="text-red-500 flex items-center space-x-2"> <span className="text-red-500 flex items-center space-x-2">
<X className="w-4 h-4" /> <X className="w-4 h-4" />
<span>Offline</span> <span>Offline</span>
</span> </span>
); );
} }
}, }
}, },
{ {
id: "actions", id: "actions",
@ -229,16 +233,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" <span className="text-red-500">Delete</span>
>
Delete
</button>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -252,15 +253,19 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</Link> </Link>
</div> </div>
); );
}, }
}, }
]; ];
return ( return (
<> <>
<CreateSiteForm <CreateSiteFormModal
open={isCreateModalOpen} open={isCreateModalOpen}
setOpen={setIsCreateModalOpen} setOpen={setIsCreateModalOpen}
onCreate={(val) => {
setRows([val, ...rows]);
}}
orgId={orgId}
/> />
{selectedSite && ( {selectedSite && (
@ -302,12 +307,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 ? (

View file

@ -32,6 +32,7 @@ import {
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
import CreateSiteForm from "../[orgId]/settings/sites/components/CreateSiteForm";
type Step = "org" | "site" | "resources"; type Step = "org" | "site" | "resources";
@ -45,6 +46,7 @@ export default function StepperForm() {
const [orgIdTaken, setOrgIdTaken] = useState(false); const [orgIdTaken, setOrgIdTaken] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const orgForm = useForm<z.infer<typeof orgSchema>>({ const orgForm = useForm<z.infer<typeof orgSchema>>({
@ -292,18 +294,38 @@ export default function StepperForm() {
)} )}
{currentStep === "site" && ( {currentStep === "site" && (
<div className="flex justify-end"> <div>
<Button <CreateSiteForm
type="submit" setLoading={(val) => setLoading(val)}
variant="outline" setChecked={(val) => setIsChecked(val)}
onClick={() => { orgId={orgForm.getValues().orgId}
onCreate={() => {
router.push( router.push(
`/${orgForm.getValues().orgId}/settings/sites` `/${orgForm.getValues().orgId}/settings/resources`
); );
}} }}
> />
Skip for now <div className="flex justify-between mt-6">
</Button> <Button
type="submit"
variant="outline"
onClick={() => {
router.push(
`/${orgForm.getValues().orgId}/settings/sites`
);
}}
>
Skip for now
</Button>
<Button
type="submit"
form="create-site-form"
loading={loading}
disabled={loading || !isChecked}
>
Create Site
</Button>
</div>
</div> </div>
)} )}
</section> </section>