mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-17 10:26:39 +00:00
Merge branch 'main' of https://github.com/fosrl/pangolin
This commit is contained in:
@@ -3,9 +3,13 @@ import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { resourceSessions, ResourceSession } from "@server/db/schema";
|
||||
import db from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/config";
|
||||
|
||||
export const SESSION_COOKIE_NAME = "resource_session";
|
||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
export const SECURE_COOKIES = config.server.secure_cookies;
|
||||
export const COOKIE_DOMAIN =
|
||||
"." + new URL(config.app.base_url).hostname.split(".").slice(-2).join(".");
|
||||
|
||||
export async function createResourceSession(opts: {
|
||||
token: string;
|
||||
@@ -115,25 +119,25 @@ export async function invalidateAllSessions(
|
||||
}
|
||||
|
||||
export function serializeResourceSessionCookie(
|
||||
cookieName: string,
|
||||
token: string,
|
||||
fqdn: string,
|
||||
secure: boolean,
|
||||
): string {
|
||||
if (secure) {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=.localhost`;
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=.localhost`;
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlankResourceSessionTokenCookie(
|
||||
cookieName: string,
|
||||
fqdn: string,
|
||||
secure: boolean,
|
||||
): string {
|
||||
if (secure) {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${fqdn}`;
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${fqdn}`;
|
||||
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,6 @@ const environmentSchema = z.object({
|
||||
secure_cookies: z.boolean(),
|
||||
signup_secret: z.string().optional(),
|
||||
session_cookie_name: z.string(),
|
||||
}),
|
||||
badger: z.object({
|
||||
session_query_parameter: z.string(),
|
||||
resource_session_cookie_name: z.string(),
|
||||
}),
|
||||
traefik: z.object({
|
||||
@@ -128,12 +125,14 @@ if (!parsedConfig.success) {
|
||||
|
||||
process.env.SERVER_EXTERNAL_PORT =
|
||||
parsedConfig.data.server.external_port.toString();
|
||||
process.env.SERVER_INTERNAL_PORT =
|
||||
parsedConfig.data.server.internal_port.toString();
|
||||
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
|
||||
?.require_email_verification
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name;
|
||||
process.env.RESOURCE_SESSION_QUERY_PARAM_NAME =
|
||||
parsedConfig.data.badger.session_query_parameter;
|
||||
process.env.RESOURCE_SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.resource_session_cookie_name;
|
||||
|
||||
export default parsedConfig.data;
|
||||
|
||||
@@ -287,16 +287,28 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
||||
() => resourcePassword.passwordId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
}
|
||||
},
|
||||
),
|
||||
pincodeId: integer("pincodeId").references(
|
||||
() => resourcePincode.pincodeId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
}
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const resourceOtp = sqliteTable("resourceOtp", {
|
||||
otpId: integer("otpId").primaryKey({
|
||||
autoIncrement: true,
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
otpHash: text("otpHash").notNull(),
|
||||
expiresAt: integer("expiresAt").notNull(),
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -325,3 +337,4 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from "@react-email/components";
|
||||
import { render } from "@react-email/render";
|
||||
import { ReactElement } from "react";
|
||||
import emailClient from "@server/emails";
|
||||
import logger from "@server/logger";
|
||||
|
||||
@@ -33,9 +33,17 @@ export const SendInviteLink = ({
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind>
|
||||
<Tailwind config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "#F97317"
|
||||
}
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Body className="font-sans">
|
||||
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8">
|
||||
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
|
||||
<Heading className="text-2xl font-semibold text-gray-800 text-center">
|
||||
You're invited to join a Fossorial organization
|
||||
</Heading>
|
||||
@@ -58,7 +66,7 @@ export const SendInviteLink = ({
|
||||
<Section className="text-center my-6">
|
||||
<Button
|
||||
href={inviteLink}
|
||||
className="rounded-md bg-gray-600 px-[12px] py-[12px] text-center font-semibold text-white cursor-pointer"
|
||||
className="rounded-lg bg-primary px-[12px] py-[9px] text-center font-semibold text-white cursor-pointer"
|
||||
>
|
||||
Accept invitation to {orgName}
|
||||
</Button>
|
||||
|
||||
@@ -14,11 +14,13 @@ import * as React from "react";
|
||||
interface VerifyEmailProps {
|
||||
username?: string;
|
||||
verificationCode: string;
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export const VerifyEmail = ({
|
||||
username,
|
||||
verificationCode,
|
||||
verifyLink,
|
||||
}: VerifyEmailProps) => {
|
||||
const previewText = `Verify your email, ${username}`;
|
||||
|
||||
@@ -26,21 +28,34 @@ export const VerifyEmail = ({
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "#F97317",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Body className="font-sans">
|
||||
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8">
|
||||
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
|
||||
<Heading className="text-2xl font-semibold text-gray-800 text-center">
|
||||
Verify Your Email
|
||||
Please verify your email
|
||||
</Heading>
|
||||
<Text className="text-base text-gray-700 mt-4">
|
||||
Hi {username || "there"},
|
||||
</Text>
|
||||
<Text className="text-base text-gray-700 mt-2">
|
||||
You’ve requested to verify your email. Please use
|
||||
the verification code below:
|
||||
You’ve requested to verify your email. Please{" "}
|
||||
<a href={verifyLink} className="text-primary">
|
||||
click here
|
||||
</a>{" "}
|
||||
to verify your email, then enter the following code:
|
||||
</Text>
|
||||
<Section className="text-center my-6">
|
||||
<Text className="inline-block bg-gray-100 text-xl font-bold text-gray-900 py-2 px-4 border border-gray-300 rounded-md">
|
||||
<Text className="inline-block bg-primary text-xl font-bold text-white py-2 px-4 border border-gray-300 rounded-xl">
|
||||
{verificationCode}
|
||||
</Text>
|
||||
</Section>
|
||||
@@ -59,3 +74,5 @@ export const VerifyEmail = ({
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmail;
|
||||
|
||||
@@ -51,7 +51,7 @@ app.prepare().then(() => {
|
||||
externalServer.use(logIncomingMiddleware);
|
||||
externalServer.use(prefix, unauthenticated);
|
||||
externalServer.use(prefix, authenticated);
|
||||
externalServer.use(`${prefix}/ws`, wsRouter);
|
||||
// externalServer.use(`${prefix}/ws`, wsRouter);
|
||||
|
||||
externalServer.use(notFoundMiddleware);
|
||||
|
||||
@@ -68,7 +68,7 @@ app.prepare().then(() => {
|
||||
);
|
||||
});
|
||||
|
||||
handleWSUpgrade(httpServer);
|
||||
// handleWSUpgrade(httpServer);
|
||||
|
||||
externalServer.use(errorHandlerMiddleware);
|
||||
|
||||
|
||||
65
server/routers/auth/checkResourceSession.ts
Normal file
65
server/routers/auth/checkResourceSession.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/utils";
|
||||
import { validateSessionToken } from "@server/auth";
|
||||
import { validateResourceSessionToken } from "@server/auth/resource";
|
||||
|
||||
export const params = z.object({
|
||||
token: z.string(),
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
export type CheckResourceSessionParams = z.infer<typeof params>;
|
||||
|
||||
export type CheckResourceSessionResponse = {
|
||||
valid: boolean;
|
||||
};
|
||||
|
||||
export async function checkResourceSession(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): Promise<any> {
|
||||
const parsedParams = params.safeParse(req.params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const { token, resourceId } = parsedParams.data;
|
||||
|
||||
try {
|
||||
const { resourceSession } = await validateResourceSessionToken(
|
||||
token,
|
||||
resourceId,
|
||||
);
|
||||
|
||||
let valid = false;
|
||||
if (resourceSession) {
|
||||
valid = true;
|
||||
}
|
||||
|
||||
return response<CheckResourceSessionResponse>(res, {
|
||||
data: { valid },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Checked validity",
|
||||
status: HttpCode.OK,
|
||||
});
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to reset password",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,4 @@ export * from "./requestEmailVerificationCode";
|
||||
export * from "./changePassword";
|
||||
export * from "./requestPasswordReset";
|
||||
export * from "./resetPassword";
|
||||
export * from "./checkResourceSession";
|
||||
|
||||
@@ -139,7 +139,7 @@ export async function login(
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Email verification code sent",
|
||||
status: HttpCode.ACCEPTED,
|
||||
status: HttpCode.OK,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,18 @@ export async function sendEmailVerificationCode(
|
||||
): Promise<void> {
|
||||
const code = await generateEmailVerificationCode(userId, email);
|
||||
|
||||
await sendEmail(VerifyEmail({ username: email, verificationCode: code }), {
|
||||
to: email,
|
||||
from: config.email?.no_reply,
|
||||
subject: "Verify your email address",
|
||||
});
|
||||
await sendEmail(
|
||||
VerifyEmail({
|
||||
username: email,
|
||||
verificationCode: code,
|
||||
verifyLink: `${config.app.base_url}/auth/verify-email`,
|
||||
}),
|
||||
{
|
||||
to: email,
|
||||
from: config.email?.no_reply,
|
||||
subject: "Verify your email address",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function generateEmailVerificationCode(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resources,
|
||||
User,
|
||||
userOrgs,
|
||||
} from "@server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
@@ -19,10 +20,7 @@ import { Resource, roleResources, userResources } from "@server/db/schema";
|
||||
import logger from "@server/logger";
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.object({
|
||||
session: z.string().nullable(),
|
||||
resource_session: z.string().nullable(),
|
||||
}),
|
||||
sessions: z.record(z.string()).optional(),
|
||||
originalRequestURL: z.string().url(),
|
||||
scheme: z.string(),
|
||||
host: z.string(),
|
||||
@@ -98,13 +96,18 @@ export async function verifyResourceSession(
|
||||
|
||||
const redirectUrl = `${config.app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
|
||||
if (sso && sessions.session) {
|
||||
const { session, user } = await validateSessionToken(
|
||||
sessions.session,
|
||||
);
|
||||
if (!sessions) {
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
const sessionToken = sessions[config.server.session_cookie_name];
|
||||
|
||||
// check for unified login
|
||||
if (sso && sessionToken) {
|
||||
const { session, user } = await validateSessionToken(sessionToken);
|
||||
if (session && user) {
|
||||
const isAllowed = await isUserAllowedToAccessResource(
|
||||
user.userId,
|
||||
user,
|
||||
resource,
|
||||
);
|
||||
|
||||
@@ -117,11 +120,17 @@ export async function verifyResourceSession(
|
||||
}
|
||||
}
|
||||
|
||||
if (password && sessions.resource_session) {
|
||||
const resourceSessionToken =
|
||||
sessions[
|
||||
`${config.server.resource_session_cookie_name}_${resource.resourceId}`
|
||||
];
|
||||
|
||||
if ((pincode || password) && resourceSessionToken) {
|
||||
const { resourceSession } = await validateResourceSessionToken(
|
||||
sessions.resource_session,
|
||||
resourceSessionToken,
|
||||
resource.resourceId,
|
||||
);
|
||||
|
||||
if (resourceSession) {
|
||||
if (
|
||||
pincode &&
|
||||
@@ -165,7 +174,7 @@ function notAllowed(res: Response, redirectUrl?: string) {
|
||||
error: false,
|
||||
message: "Access denied",
|
||||
status: HttpCode.OK,
|
||||
}
|
||||
};
|
||||
logger.debug(JSON.stringify(data));
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
@@ -177,21 +186,25 @@ function allowed(res: Response) {
|
||||
error: false,
|
||||
message: "Access allowed",
|
||||
status: HttpCode.OK,
|
||||
}
|
||||
};
|
||||
logger.debug(JSON.stringify(data));
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
|
||||
async function isUserAllowedToAccessResource(
|
||||
userId: string,
|
||||
user: User,
|
||||
resource: Resource,
|
||||
) {
|
||||
): Promise<boolean> {
|
||||
if (config.flags?.require_email_verification && !user.emailVerified) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.userId, user.userId),
|
||||
eq(userOrgs.orgId, resource.orgId),
|
||||
),
|
||||
)
|
||||
@@ -221,7 +234,7 @@ async function isUserAllowedToAccessResource(
|
||||
.from(userResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
eq(userResources.userId, user.userId),
|
||||
eq(userResources.resourceId, resource.resourceId),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router } from "express";
|
||||
import * as gerbil from "@server/routers/gerbil";
|
||||
import * as badger from "@server/routers/badger";
|
||||
import * as traefik from "@server/routers/traefik";
|
||||
import * as auth from "@server/routers/auth";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
// Root routes
|
||||
@@ -12,6 +13,10 @@ internalRouter.get("/", (_, res) => {
|
||||
});
|
||||
|
||||
internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
||||
internalRouter.get(
|
||||
"/resource-session/:resourceId/:token",
|
||||
auth.checkResourceSession,
|
||||
);
|
||||
|
||||
// Gerbil routes
|
||||
const gerbilRouter = Router();
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
serializeResourceSessionCookie,
|
||||
} from "@server/auth/resource";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/config";
|
||||
|
||||
export const authWithPasswordBodySchema = z.object({
|
||||
password: z.string(),
|
||||
@@ -131,15 +132,15 @@ export async function authWithPassword(
|
||||
token,
|
||||
passwordId: definedPassword.passwordId,
|
||||
});
|
||||
// const secureCookie = resource.ssl;
|
||||
// const cookie = serializeResourceSessionCookie(
|
||||
// token,
|
||||
// resource.fullDomain,
|
||||
// secureCookie,
|
||||
// );
|
||||
// res.appendHeader("Set-Cookie", cookie);
|
||||
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
token,
|
||||
resource.fullDomain,
|
||||
);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
// logger.debug(cookie); // remove after testing
|
||||
logger.debug(cookie); // remove after testing
|
||||
|
||||
return response<AuthWithPasswordResponse>(res, {
|
||||
data: {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
serializeResourceSessionCookie,
|
||||
} from "@server/auth/resource";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/config";
|
||||
|
||||
export const authWithPincodeBodySchema = z.object({
|
||||
pincode: z.string(),
|
||||
@@ -127,15 +128,15 @@ export async function authWithPincode(
|
||||
token,
|
||||
pincodeId: definedPincode.pincodeId,
|
||||
});
|
||||
// const secureCookie = resource.ssl;
|
||||
// const cookie = serializeResourceSessionCookie(
|
||||
// token,
|
||||
// resource.fullDomain,
|
||||
// secureCookie,
|
||||
// );
|
||||
// res.appendHeader("Set-Cookie", cookie);
|
||||
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
token,
|
||||
resource.fullDomain,
|
||||
);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
// logger.debug(cookie); // remove after testing
|
||||
logger.debug(cookie); // remove after testing
|
||||
|
||||
return response<AuthWithPincodeResponse>(res, {
|
||||
data: {
|
||||
|
||||
@@ -94,6 +94,7 @@ export async function createResource(
|
||||
orgId,
|
||||
name,
|
||||
subdomain,
|
||||
ssl: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
sites,
|
||||
userResources,
|
||||
roleResources,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
} from "@server/db/schema";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -46,39 +48,65 @@ const listResourcesSchema = z.object({
|
||||
function queryResources(
|
||||
accessibleResourceIds: number[],
|
||||
siteId?: number,
|
||||
orgId?: string
|
||||
orgId?: string,
|
||||
) {
|
||||
if (siteId) {
|
||||
return db
|
||||
.select({
|
||||
resourceId: resources.resourceId,
|
||||
name: resources.name,
|
||||
subdomain: resources.subdomain,
|
||||
fullDomain: resources.fullDomain,
|
||||
ssl: resources.ssl,
|
||||
siteName: sites.name,
|
||||
siteId: sites.niceId,
|
||||
passwordId: resourcePassword.passwordId,
|
||||
pincodeId: resourcePincode.pincodeId,
|
||||
sso: resources.sso,
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId),
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(resources.resourceId, accessibleResourceIds),
|
||||
eq(resources.siteId, siteId)
|
||||
)
|
||||
eq(resources.siteId, siteId),
|
||||
),
|
||||
);
|
||||
} else if (orgId) {
|
||||
return db
|
||||
.select({
|
||||
resourceId: resources.resourceId,
|
||||
name: resources.name,
|
||||
subdomain: resources.subdomain,
|
||||
ssl: resources.ssl,
|
||||
fullDomain: resources.fullDomain,
|
||||
siteName: sites.name,
|
||||
siteId: sites.niceId,
|
||||
passwordId: resourcePassword.passwordId,
|
||||
sso: resources.sso,
|
||||
pincodeId: resourcePincode.pincodeId,
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId),
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(resources.resourceId, accessibleResourceIds),
|
||||
eq(resources.orgId, orgId)
|
||||
)
|
||||
eq(resources.orgId, orgId),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,7 +119,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);
|
||||
@@ -99,8 +127,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;
|
||||
@@ -110,8 +138,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;
|
||||
@@ -120,8 +148,8 @@ 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",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,17 +160,17 @@ export async function listResources(
|
||||
.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
|
||||
@@ -173,7 +201,10 @@ export async function listResources(
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,15 @@ function querySites(orgId: string, accessibleSiteIds: number[]) {
|
||||
megabytesIn: sites.megabytesIn,
|
||||
megabytesOut: sites.megabytesOut,
|
||||
orgName: orgs.name,
|
||||
type: sites.type,
|
||||
})
|
||||
.from(sites)
|
||||
.leftJoin(orgs, eq(sites.orgId, orgs.orgId))
|
||||
.where(
|
||||
and(
|
||||
inArray(sites.siteId, accessibleSiteIds),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
eq(sites.orgId, orgId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +58,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);
|
||||
@@ -65,8 +66,8 @@ export async function listSites(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
fromError(parsedQuery.error),
|
||||
),
|
||||
);
|
||||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
@@ -76,8 +77,8 @@ export async function listSites(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
fromError(parsedParams.error),
|
||||
),
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
@@ -86,8 +87,8 @@ 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",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,8 +101,8 @@ export async function listSites(
|
||||
.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);
|
||||
@@ -113,8 +114,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);
|
||||
@@ -137,7 +138,10 @@ export async function listSites(
|
||||
});
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ export async function traefikConfigProvider(
|
||||
schema.orgs,
|
||||
eq(schema.resources.orgId, schema.orgs.orgId),
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sites,
|
||||
eq(schema.sites.siteId, schema.resources.siteId),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.targets.enabled, true),
|
||||
@@ -51,11 +55,9 @@ export async function traefikConfigProvider(
|
||||
`http://${config.server.internal_hostname}:${config.server.internal_port}`,
|
||||
).href,
|
||||
resourceSessionCookieName:
|
||||
config.badger.resource_session_cookie_name,
|
||||
config.server.resource_session_cookie_name,
|
||||
userSessionCookieName:
|
||||
config.server.session_cookie_name,
|
||||
sessionQueryParameter:
|
||||
config.badger.session_query_parameter,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -70,6 +72,7 @@ export async function traefikConfigProvider(
|
||||
for (const item of all) {
|
||||
const target = item.targets;
|
||||
const resource = item.resources;
|
||||
const site = item.sites;
|
||||
const org = item.orgs;
|
||||
|
||||
const routerName = `${target.targetId}-router`;
|
||||
@@ -112,7 +115,7 @@ export async function traefikConfigProvider(
|
||||
? config.traefik.https_entrypoint
|
||||
: config.traefik.http_entrypoint,
|
||||
],
|
||||
middlewares: resource.ssl ? [badgerMiddlewareName] : [],
|
||||
middlewares: [badgerMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: `Host(\`${fullDomain}\`)`,
|
||||
...(resource.ssl ? { tls } : {}),
|
||||
@@ -128,15 +131,28 @@ export async function traefikConfigProvider(
|
||||
};
|
||||
}
|
||||
|
||||
http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `${target.method}://${target.ip}:${target.port}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
if (site.type === "newt") {
|
||||
const ip = site.subnet.split("/")[0];
|
||||
http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `${target.method}://${ip}:${target.internalPort}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} else if (site.type === "wireguard") {
|
||||
http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `${target.method}://${target.ip}:${target.port}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(HttpCode.OK).json({ http });
|
||||
|
||||
@@ -86,7 +86,6 @@ export async function inviteUser(
|
||||
|
||||
inviteTracker[email].timestamps.push(currentTime);
|
||||
|
||||
logger.debug("here0")
|
||||
const org = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
@@ -98,7 +97,6 @@ export async function inviteUser(
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug("here1")
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
@@ -114,7 +112,6 @@ export async function inviteUser(
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug("here2")
|
||||
const inviteId = generateRandomString(
|
||||
10,
|
||||
alphabet("a-z", "A-Z", "0-9"),
|
||||
@@ -124,7 +121,6 @@ export async function inviteUser(
|
||||
|
||||
const tokenHash = await hashPassword(token);
|
||||
|
||||
logger.debug("here3")
|
||||
// delete any existing invites for this email
|
||||
await db
|
||||
.delete(userInvites)
|
||||
@@ -133,7 +129,6 @@ export async function inviteUser(
|
||||
)
|
||||
.execute();
|
||||
|
||||
logger.debug("here4")
|
||||
await db.insert(userInvites).values({
|
||||
inviteId,
|
||||
orgId,
|
||||
@@ -145,23 +140,21 @@ export async function inviteUser(
|
||||
|
||||
const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`;
|
||||
|
||||
logger.debug("here5")
|
||||
// await sendEmail(
|
||||
// SendInviteLink({
|
||||
// email,
|
||||
// inviteLink,
|
||||
// expiresInDays: (validHours / 24).toString(),
|
||||
// orgName: org[0].name || orgId,
|
||||
// inviterName: req.user?.email,
|
||||
// }),
|
||||
// {
|
||||
// to: email,
|
||||
// from: config.email?.no_reply,
|
||||
// subject: "You're invited to join a Fossorial organization",
|
||||
// },
|
||||
// );
|
||||
await sendEmail(
|
||||
SendInviteLink({
|
||||
email,
|
||||
inviteLink,
|
||||
expiresInDays: (validHours / 24).toString(),
|
||||
orgName: org[0].name || orgId,
|
||||
inviterName: req.user?.email,
|
||||
}),
|
||||
{
|
||||
to: email,
|
||||
from: config.email?.no_reply,
|
||||
subject: "You're invited to join a Fossorial organization",
|
||||
},
|
||||
);
|
||||
|
||||
logger.debug("here6")
|
||||
return response<InviteUserResponse>(res, {
|
||||
data: {
|
||||
inviteLink,
|
||||
|
||||
Reference in New Issue
Block a user