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,7 +25,8 @@ gerbil:
block_size: 16
subnet_group: 10.0.0.0/8
rate_limit:
rate_limits:
global:
window_minutes: 1
max_requests: 100

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,9 +18,11 @@ import { fromError } from "zod-validation-error";
import { sendToClient } from "../ws";
import { deletePeer } from "../gerbil/peers";
const deleteOrgSchema = z.object({
const deleteOrgSchema = z
.object({
orgId: z.string()
});
})
.strict();
export async function deleteOrg(
req: Request,

View file

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

View file

@ -9,9 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const updateOrgParamsSchema = z.object({
orgId: z.string(),
});
const updateOrgParamsSchema = z
.object({
orgId: z.string()
})
.strict();
const updateOrgBodySchema = z
.object({
@ -20,7 +22,7 @@ const updateOrgBodySchema = z
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
message: "At least one field must be provided for update"
});
export async function updateOrg(
@ -72,7 +74,7 @@ export async function updateOrg(
success: true,
error: false,
message: "Organization updated successfully",
status: HttpCode.OK,
status: HttpCode.OK
});
} catch (error) {
logger.error(error);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,9 +10,14 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/schemas/subdomainSchema";
const updateResourceParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
const updateResourceParamsSchema = z
.object({
resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
const updateResourceBodySchema = z
.object({
@ -21,18 +26,18 @@ const updateResourceBodySchema = z
ssl: z.boolean().optional(),
sso: z.boolean().optional(),
blockAccess: z.boolean().optional(),
emailWhitelistEnabled: z.boolean().optional(),
emailWhitelistEnabled: z.boolean().optional()
// siteId: z.number(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
message: "At least one field must be provided for update"
});
export async function updateResource(
req: Request,
res: Response,
next: NextFunction,
next: NextFunction
): Promise<any> {
try {
const parsedParams = updateResourceParamsSchema.safeParse(req.params);
@ -40,8 +45,8 @@ export async function updateResource(
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString(),
),
fromError(parsedParams.error).toString()
)
);
}
@ -50,8 +55,8 @@ export async function updateResource(
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString(),
),
fromError(parsedBody.error).toString()
)
);
}
@ -68,8 +73,8 @@ export async function updateResource(
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`,
),
`Resource with ID ${resourceId} not found`
)
);
}
@ -77,8 +82,8 @@ export async function updateResource(
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Resource does not have a domain",
),
"Resource does not have a domain"
)
);
}
@ -88,7 +93,7 @@ export async function updateResource(
const updatePayload = {
...updateData,
...(fullDomain && { fullDomain }),
...(fullDomain && { fullDomain })
};
const updatedResource = await db
@ -101,8 +106,8 @@ export async function updateResource(
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`,
),
`Resource with ID ${resourceId} not found`
)
);
}
@ -111,15 +116,12 @@ export async function updateResource(
success: true,
error: false,
message: "Resource updated successfully",
status: HttpCode.OK,
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred",
),
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -197,12 +197,16 @@ export default function CreateShareLinkForm({
const link = constructShareLink(
values.resourceId,
token.accessTokenId,
token.tokenHash
token.accessToken
);
setLink(link);
onCreated?.({
...token,
resourceName: values.resourceName
accessTokenId: token.accessTokenId,
resourceId: token.resourceId,
resourceName: values.resourceName,
title: token.title,
createdAt: token.createdAt,
expiresAt: token.expiresAt
});
}
@ -285,7 +289,9 @@ export default function CreateShareLinkForm({
r
) => (
<CommandItem
value={r.name}
value={
r.name
}
key={
r.resourceId
}
@ -441,6 +447,10 @@ export default function CreateShareLinkForm({
)}
{link && (
<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>
Anyone with this link can access the
resource. Share it with care.

View file

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

View file

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

View file

@ -1,6 +1,5 @@
"use client";
import { Button, buttonVariants } from "@app/components/ui/button";
import {
Form,
FormControl,
@ -8,7 +7,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { useToast } from "@app/hooks/useToast";
@ -16,19 +15,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
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 { CreateSiteBody, PickSiteDefaultsResponse } from "@server/routers/site";
import {
CreateSiteBody,
CreateSiteResponse,
PickSiteDefaultsResponse
} from "@server/routers/site";
import { generateKeypair } from "../[niceId]/components/wireguardConfig";
import CopyTextBox from "@app/components/CopyTextBox";
import { Checkbox } from "@app/components/ui/checkbox";
@ -37,84 +29,92 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectValue
} from "@app/components/ui/select";
import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { SiteRow } from "./SitesTable";
import { AxiosResponse } from "axios";
const method = [
{ label: "Newt", value: "newt" },
{ label: "Wireguard", value: "wireguard" },
] as const;
const accountFormSchema = z.object({
const createSiteFormSchema = z.object({
name: z
.string()
.min(2, {
message: "Name must be at least 2 characters.",
message: "Name must be at least 2 characters."
})
.max(30, {
message: "Name must not be longer than 30 characters.",
message: "Name must not be longer than 30 characters."
}),
method: z.enum(["wireguard", "newt"]),
method: z.enum(["wireguard", "newt"])
});
type AccountFormValues = z.infer<typeof accountFormSchema>;
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
const defaultValues: Partial<AccountFormValues> = {
const defaultValues: Partial<CreateSiteFormValues> = {
name: "",
method: "newt",
method: "newt"
};
type CreateSiteFormProps = {
open: boolean;
setOpen: (open: boolean) => void;
onCreate?: (site: SiteRow) => 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 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 [keypair, setKeypair] = useState<{
publicKey: string;
privateKey: string;
} | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isChecked, setIsChecked] = useState(false);
const [siteDefaults, setSiteDefaults] =
useState<PickSiteDefaultsResponse | null>(null);
const handleCheckboxChange = (checked: boolean) => {
setChecked?.(checked);
setIsChecked(checked);
};
const form = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema),
defaultValues,
const form = useForm<CreateSiteFormValues>({
resolver: zodResolver(createSiteFormSchema),
defaultValues
});
useEffect(() => {
if (!open) return;
if (typeof window !== "undefined") {
// reset all values
setLoading?.(false);
setIsLoading(false);
form.reset();
setChecked?.(false);
setKeypair(null);
setSiteDefaults(null);
const generatedKeypair = generateKeypair();
setKeypair(generatedKeypair);
setIsLoading(false);
api.get(`/org/${orgId}/pick-site-defaults`)
.catch((e) => {
toast({
variant: "destructive",
title: "Error picking site defaults",
description: formatAxiosError(e),
description: formatAxiosError(e)
});
})
.then((res) => {
@ -122,11 +122,11 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
setSiteDefaults(res.data.data);
}
});
}
}, [open]);
async function onSubmit(data: AccountFormValues) {
setLoading(true);
async function onSubmit(data: CreateSiteFormValues) {
setLoading?.(true);
setIsLoading(true);
if (!siteDefaults || !keypair) {
return;
}
@ -135,32 +135,45 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
subnet: siteDefaults.subnet,
exitNodeId: siteDefaults.exitNodeId,
pubKey: keypair.publicKey,
type: data.method,
type: data.method
};
if (data.method === "newt") {
payload.secret = siteDefaults.newtSecret;
payload.newtId = siteDefaults.newtId;
}
const res = await api
.put(`/org/${orgId}/site/`, payload)
.put<
AxiosResponse<CreateSiteResponse>
>(`/org/${orgId}/site/`, payload)
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating site",
description: formatAxiosError(e),
description: formatAxiosError(e)
});
});
if (res && res.status === 201) {
const niceId = res.data.data.niceId;
// navigate to the site page
router.push(`/${orgId}/settings/sites/${niceId}`);
// router.push(`/${orgId}/settings/sites/${niceId}`);
// close the modal
setOpen(false);
const data = res.data.data;
onCreate?.({
name: data.name,
id: data.siteId,
nice: data.niceId.toString(),
mbIn: "0 MB",
mbOut: "0 MB",
orgId: orgId as string,
type: data.type as any,
online: false
});
}
setLoading(false);
setLoading?.(false);
setIsLoading(false);
}
const wgConfig =
@ -186,28 +199,6 @@ PersistentKeepalive = 5`
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
return (
<>
<Credenza
open={open}
onOpenChange={(val) => {
setOpen(val);
setLoading(false);
// reset all values
form.reset();
setIsChecked(false);
setKeypair(null);
setSiteDefaults(null);
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Create Site</CredenzaTitle>
<CredenzaDescription>
Create a new site to start connecting your resources
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-8">
<Form {...form}>
<form
@ -229,8 +220,8 @@ PersistentKeepalive = 5`
/>
</FormControl>
<FormDescription>
This is the name that will
be displayed for this site.
This is the name that will be displayed for
this site.
</FormDescription>
<FormMessage />
</FormItem>
@ -245,9 +236,7 @@ PersistentKeepalive = 5`
<FormControl>
<Select
value={field.value}
onValueChange={
field.onChange
}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="Select method" />
@ -263,44 +252,33 @@ PersistentKeepalive = 5`
</Select>
</FormControl>
<FormDescription>
This is how you will connect
your site to Fossorial.
This is how you will expose connections.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="max-w-md">
{form.watch("method") === "wireguard" &&
!isLoading ? (
<div className="w-full">
{form.watch("method") === "wireguard" && !isLoading ? (
<CopyTextBox text={wgConfig} />
) : form.watch("method") === "wireguard" &&
isLoading ? (
<p>
Loading WireGuard
configuration...
</p>
<p>Loading WireGuard configuration...</p>
) : (
<CopyTextBox
text={newtConfig}
wrapText={false}
/>
<CopyTextBox text={newtConfig} wrapText={false} />
)}
</div>
<span className="text-sm text-muted-foreground">
You will only be able to see the
configuration once.
You will only be able to see the configuration once.
</span>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={isChecked}
onCheckedChange={
handleCheckboxChange
}
onCheckedChange={handleCheckboxChange}
/>
<label
htmlFor="terms"
@ -312,22 +290,5 @@ PersistentKeepalive = 5`
</form>
</Form>
</div>
</CredenzaBody>
<CredenzaFooter>
<Button
type="submit"
form="create-site-form"
loading={loading}
disabled={loading || !isChecked}
>
Create Site
</Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter>
</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,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button";
import { ArrowRight, ArrowUpDown, Check, MoreHorizontal, X } from "lucide-react";
import {
ArrowRight,
ArrowUpDown,
Check,
MoreHorizontal,
X
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { AxiosResponse } from "axios";
@ -20,6 +26,7 @@ import { useToast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import CreateSiteFormModal from "./CreateSiteModal";
export type SiteRow = {
id: number;
@ -45,14 +52,10 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
const [rows, setRows] = useState<SiteRow[]>(sites);
const api = createApiClient(useEnvContext());
const callApi = async () => {
const res = await api.put<AxiosResponse<any>>(`/newt`);
console.log(res);
};
const deleteSite = (siteId: number) => {
api.delete(`/site/${siteId}`)
.catch((e) => {
@ -60,12 +63,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
toast({
variant: "destructive",
title: "Error deleting site",
description: formatAxiosError(e, "Error deleting site"),
description: formatAxiosError(e, "Error deleting site")
});
})
.then(() => {
router.refresh();
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" />
</Button>
);
},
}
},
{
accessorKey: "nice",
@ -100,7 +105,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
}
},
{
accessorKey: "mbIn",
@ -116,7 +121,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
}
},
{
accessorKey: "mbOut",
@ -132,7 +137,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
}
},
{
accessorKey: "type",
@ -167,7 +172,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</div>
);
}
},
}
},
{
accessorKey: "online",
@ -186,7 +191,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
},
cell: ({ row }) => {
const originalRow = row.original;
console.log(originalRow.online);
if (originalRow.online) {
return (
@ -203,7 +207,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</span>
);
}
},
}
},
{
id: "actions",
@ -229,16 +233,13 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
View settings
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<button
<DropdownMenuItem
onClick={() => {
setSelectedSite(siteRow);
setIsDeleteModalOpen(true);
}}
className="text-red-500"
>
Delete
</button>
<span className="text-red-500">Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -252,15 +253,19 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</Link>
</div>
);
},
},
}
}
];
return (
<>
<CreateSiteForm
<CreateSiteFormModal
open={isCreateModalOpen}
setOpen={setIsCreateModalOpen}
onCreate={(val) => {
setRows([val, ...rows]);
}}
orgId={orgId}
/>
{selectedSite && (
@ -302,12 +307,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<SitesDataTable
columns={columns}
data={sites}
data={rows}
addSite={() => {
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 { cookies } from "next/headers";
import { CheckResourceSessionResponse } from "@server/routers/auth";
import AccessTokenInvalid from "./components/AccessTokenInvalid";
import AccessTokenInvalid from "./components/AccessToken";
import AccessToken from "./components/AccessToken";
export default async function ResourceAuthPage(props: {
params: Promise<{ resourceId: number }>;
@ -50,35 +51,6 @@ export default async function ResourceAuthPage(props: {
const redirectUrl = searchParams.redirect || authInfo.url;
if (searchParams.token) {
let doRedirect = false;
try {
const res = await internal.post<
AxiosResponse<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 =
authInfo.password ||
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 (
<>
{userIsUnauthorized && isSSOOnly ? (

View file

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