This commit is contained in:
Owen Schwartz
2024-12-01 19:46:01 -05:00
57 changed files with 957 additions and 485 deletions

View File

@@ -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}`;
}
}

View File

@@ -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;

View File

@@ -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>;

View File

@@ -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";

View File

@@ -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>

View File

@@ -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">
Youve requested to verify your email. Please use
the verification code below:
Youve 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;

View File

@@ -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);

View 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",
),
);
}
}

View File

@@ -9,3 +9,4 @@ export * from "./requestEmailVerificationCode";
export * from "./changePassword";
export * from "./requestPasswordReset";
export * from "./resetPassword";
export * from "./checkResourceSession";

View File

@@ -139,7 +139,7 @@ export async function login(
success: true,
error: false,
message: "Email verification code sent",
status: HttpCode.ACCEPTED,
status: HttpCode.OK,
});
}

View File

@@ -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(

View File

@@ -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),
),
)

View File

@@ -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();

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -94,6 +94,7 @@ export async function createResource(
orgId,
name,
subdomain,
ssl: true,
})
.returning();

View File

@@ -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",
),
);
}
}

View File

@@ -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",
),
);
}
}

View File

@@ -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 });

View File

@@ -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,