diff --git a/config.example.yml b/config.example.yml index c8c6ee4f..8184e1a5 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,27 +1,25 @@ app: - base_url: http://localhost:3000 + base_url: https://fossorial.io log_level: debug save_logs: false server: external_port: 3000 internal_port: 3001 - internal_hostname: localhost + internal_hostname: pangolin secure_cookies: false session_cookie_name: session + resource_session_cookie_name: resource_session traefik: cert_resolver: letsencrypt http_entrypoint: web https_entrypoint: websecure - -badger: - session_query_parameter: __pang_sess - resource_session_cookie_name: resource_session + prefer_wildcard_cert: true gerbil: start_port: 51820 - base_endpoint: 127.0.0.1 + base_endpoint: fossorial.io use_subdomain: false block_size: 16 subnet_group: 10.0.0.0/8 diff --git a/package.json b/package.json index 26a7d454..e817ac04 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "private": true, "type": "module", "scripts": { - "dev": "ENVIRONMENT=dev tsx watch server/index.ts", + "dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts", "db:generate": "drizzle-kit generate", "db:push": "npx tsx server/db/migrate.ts", "db:hydrate": "npx tsx scripts/hydrate.ts", "db:studio": "drizzle-kit studio", "build": "mkdir -p dist && next build && node scripts/esbuild.mjs -e server/index.ts -o dist/server.mjs", - "start": "ENVIRONMENT=prod node dist/server.mjs", + "start": "NODE_ENV=development ENVIRONMENT=prod node dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3002" }, "dependencies": { diff --git a/server/auth/resource.ts b/server/auth/resource.ts index f9697a70..b13b54d9 100644 --- a/server/auth/resource.ts +++ b/server/auth/resource.ts @@ -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}`; } } diff --git a/server/config.ts b/server/config.ts index 62bdfab4..ee1cc3ea 100644 --- a/server/config.ts +++ b/server/config.ts @@ -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; diff --git a/server/db/schema.ts b/server/db/schema.ts index 2c462bb5..26ea4b0c 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -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; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -325,3 +337,4 @@ export type UserOrg = InferSelectModel; export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; +export type ResourceOtp = InferSelectModel; diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index f8719e0f..973f48ee 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -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"; diff --git a/server/emails/templates/SendInviteLink.tsx b/server/emails/templates/SendInviteLink.tsx index cc05881d..fabcffbd 100644 --- a/server/emails/templates/SendInviteLink.tsx +++ b/server/emails/templates/SendInviteLink.tsx @@ -33,9 +33,17 @@ export const SendInviteLink = ({ {previewText} - + - + You're invited to join a Fossorial organization @@ -58,7 +66,7 @@ export const SendInviteLink = ({
diff --git a/server/emails/templates/VerifyEmailCode.tsx b/server/emails/templates/VerifyEmailCode.tsx index dcae2971..fc8978ed 100644 --- a/server/emails/templates/VerifyEmailCode.tsx +++ b/server/emails/templates/VerifyEmailCode.tsx @@ -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 = ({ {previewText} - + - + - Verify Your Email + Please verify your email Hi {username || "there"}, - You’ve requested to verify your email. Please use - the verification code below: + You’ve requested to verify your email. Please{" "} + + click here + {" "} + to verify your email, then enter the following code:
- + {verificationCode}
@@ -59,3 +74,5 @@ export const VerifyEmail = ({ ); }; + +export default VerifyEmail; diff --git a/server/index.ts b/server/index.ts index 909bf796..f1c5e79b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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); diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts new file mode 100644 index 00000000..11f4ca90 --- /dev/null +++ b/server/routers/auth/checkResourceSession.ts @@ -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; + +export type CheckResourceSessionResponse = { + valid: boolean; +}; + +export async function checkResourceSession( + req: Request, + res: Response, + next: NextFunction, +): Promise { + 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(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", + ), + ); + } +} diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index b6ce3a01..b2eaf8d2 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -9,3 +9,4 @@ export * from "./requestEmailVerificationCode"; export * from "./changePassword"; export * from "./requestPasswordReset"; export * from "./resetPassword"; +export * from "./checkResourceSession"; diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 9ffe651c..b7b68e47 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -139,7 +139,7 @@ export async function login( success: true, error: false, message: "Email verification code sent", - status: HttpCode.ACCEPTED, + status: HttpCode.OK, }); } diff --git a/server/routers/auth/sendEmailVerificationCode.ts b/server/routers/auth/sendEmailVerificationCode.ts index 3a167405..334eb23a 100644 --- a/server/routers/auth/sendEmailVerificationCode.ts +++ b/server/routers/auth/sendEmailVerificationCode.ts @@ -13,11 +13,18 @@ export async function sendEmailVerificationCode( ): Promise { 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( diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index a733c01e..46fe6df3 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -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(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(res, data); } async function isUserAllowedToAccessResource( - userId: string, + user: User, resource: Resource, -) { +): Promise { + 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), ), ) diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 42aa7d47..fb0e1809 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -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(); diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index d803f279..4e1ae35e 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -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(res, { data: { diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 8c618f8a..8d69ede1 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -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(res, { data: { diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 50473fad..002e5434 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -94,6 +94,7 @@ export async function createResource( orgId, name, subdomain, + ssl: true, }) .returning(); diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 10f19a8e..1bc1e896 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -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 { 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", + ), ); } } diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 83adefa2..40d84b88 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -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 { 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", + ), ); } } diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 1051659a..b73bc8f7 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -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 }); diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 546ae479..84c6eb45 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -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(res, { data: { inviteLink, diff --git a/src/api/index.ts b/src/api/index.ts index 4da1a36e..d0c687f1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -22,4 +22,12 @@ export const internal = axios.create({ }, }); +export const priv = axios.create({ + baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + export default api; diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index e02e8af2..cc0d59be 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -1,6 +1,8 @@ import { internal } from "@app/api"; import { authCookieHeader } from "@app/api/cookies"; +import { verifySession } from "@app/lib/auth/verifySession"; import { GetOrgResponse } from "@server/routers/org"; +import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { cache } from "react"; @@ -17,6 +19,25 @@ export default async function OrgLayout(props: { redirect(`/`); } + const getUser = cache(verifySession); + const user = await getUser(); + + if (!user) { + redirect(`/?redirect=/${orgId}`); + } + + try { + const getOrgUser = cache(() => + internal.get>( + `/org/${orgId}/user/${user.userId}`, + cookie, + ), + ); + const orgUser = await getOrgUser(); + } catch { + redirect(`/`); + } + try { const getOrg = cache(() => internal.get>( diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 730e6851..cb3297d3 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -14,27 +14,6 @@ export default async function OrgPage(props: OrgPageProps) { const params = await props.params; const orgId = params.orgId; - const getUser = cache(verifySession); - const user = await getUser(); - - if (!user) { - redirect("/auth/login"); - } - - const cookie = await authCookieHeader(); - - try { - const getOrgUser = cache(() => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - cookie - ) - ); - const orgUser = await getOrgUser(); - } catch { - redirect(`/`); - } - return ( <>

Welcome to {orgId} dashboard

diff --git a/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx b/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx index dfae7275..8c1053d6 100644 --- a/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx +++ b/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx @@ -1,5 +1,6 @@ "use client"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { SidebarSettings } from "@app/components/SidebarSettings"; type AccessPageHeaderAndNavProps = { @@ -22,16 +23,12 @@ export default function AccessPageHeaderAndNav({ return ( <> - {" "} -
-

- Users & Roles -

-

- Invite users and add them to roles to manage access to your - organization -

-
+ + {children} diff --git a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx index 26abf881..315d0bcc 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx @@ -22,7 +22,7 @@ import { import { Button } from "@app/components/ui/button"; import { useState } from "react"; import { Input } from "@app/components/ui/input"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; import { DataTablePagination } from "@app/components/DataTablePagination"; interface DataTableProps { @@ -61,19 +61,23 @@ export function RolesDataTable({ return (
- - table - .getColumn("name") - ?.setFilterValue(event.target.value) - } - className="max-w-sm mr-2" - /> +
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="w-full pl-8" + /> + +
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -97,7 +101,7 @@ export function RolesDataTable({ : flexRender( header.column.columnDef .header, - header.getContext() + header.getContext(), )} ); @@ -118,7 +122,7 @@ export function RolesDataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} diff --git a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx index 31dde81f..f59e8cb9 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx @@ -65,6 +65,14 @@ export default function UsersTable({ roles: r }: RolesTableProps) { return ( <>
+ {roleRow.isAdmin && ( + + )} {!roleRow.isAdmin && ( @@ -81,7 +89,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -98,7 +101,7 @@ export function UsersDataTable({ : flexRender( header.column.columnDef .header, - header.getContext() + header.getContext(), )} ); @@ -119,7 +122,7 @@ export function UsersDataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} diff --git a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx index ca6afda2..0613a7d5 100644 --- a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx @@ -99,7 +99,9 @@ export default function UsersTable({ users: u }: UsersTableProps) { return (
- {userRow.isOwner && } + {userRow.isOwner && ( + + )} {userRow.role}
); @@ -113,6 +115,14 @@ export default function UsersTable({ users: u }: UsersTableProps) { return ( <>
+ {userRow.isOwner && ( + + )} {!userRow.isOwner && ( <> @@ -138,13 +148,13 @@ export default function UsersTable({ users: u }: UsersTableProps) { {userRow.email !== user?.email && ( + + )}
@@ -185,7 +194,7 @@ 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.", ), }); }); @@ -198,7 +207,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { }); setUsers((prev) => - prev.filter((u) => u.id !== selectedUser?.id) + prev.filter((u) => u.id !== selectedUser?.id), ); } } diff --git a/src/app/[orgId]/settings/components/Header.tsx b/src/app/[orgId]/settings/components/Header.tsx index 863424ca..35a7f22a 100644 --- a/src/app/[orgId]/settings/components/Header.tsx +++ b/src/app/[orgId]/settings/components/Header.tsx @@ -149,7 +149,7 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) { size="lg" role="combobox" aria-expanded={open} - className="w-full md:w-[200px] h-12 px-3 py-4 bg-neutral" + className="w-full md:w-[200px] h-12 px-3 py-4 bg-neutral hover:bg-neutral" >
@@ -202,29 +202,6 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) { - - {/* */}
diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index ee413b9e..f4dc6549 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({ const user = await getUser(); if (!user) { - redirect("/auth/login"); + redirect(`/?redirect=/${orgId}/settings/general`); } let orgUser = null; @@ -34,8 +34,8 @@ export default async function GeneralSettingsPage({ const getOrgUser = cache(async () => internal.get>( `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) + await authCookieHeader(), + ), ); const res = await getOrgUser(); orgUser = res.data.data; @@ -48,8 +48,8 @@ export default async function GeneralSettingsPage({ const getOrg = cache(async () => internal.get>( `/org/${orgId}`, - await authCookieHeader() - ) + await authCookieHeader(), + ), ); const res = await getOrg(); org = res.data.data; diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index b6ea8448..b277b7f3 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -55,7 +55,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const user = await getUser(); if (!user) { - redirect("/auth/login"); + redirect(`/?redirect=/${params.orgId}/`); } const cookie = await authCookieHeader(); @@ -64,8 +64,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const getOrgUser = cache(() => internal.get>( `/org/${params.orgId}/user/${user.userId}`, - cookie - ) + cookie, + ), ); const orgUser = await getOrgUser(); @@ -79,7 +79,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { let orgs: ListOrgsResponse["orgs"] = []; try { const getOrgs = cache(() => - internal.get>(`/orgs`, cookie) + internal.get>(`/orgs`, cookie), ); const res = await getOrgs(); if (res && res.data.data.orgs) { @@ -91,7 +91,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <> -
+
)} -
+
@@ -320,11 +320,13 @@ export default function ResourceAuthenticationPage() { defaultChecked={resource.sso} onCheckedChange={(val) => setSsoEnabled(val)} /> - +
Existing users will only have to login once for all - resources that have SSO enabled. + resources that have this enabled.
@@ -460,7 +462,7 @@ export default function ResourceAuthenticationPage() { -
+
[] = [ + { + accessorKey: "method", + header: "Method", + cell: ({ row }) => ( + + ), + }, { accessorKey: "ip", header: "IP Address", @@ -262,24 +280,6 @@ export default function ReverseProxyTargets(props: { /> ), }, - { - accessorKey: "method", - header: "Method", - cell: ({ row }) => ( - - ), - }, // { // accessorKey: "protocol", // header: "Protocol", @@ -368,7 +368,7 @@ export default function ReverseProxyTargets(props: {
-
+
- ( - - - IP Address - - - - - - Enter the IP address of the - target. - - - - )} - /> )} /> + ( + + + IP Address + + + + + + Enter the IP address of the + target. + + + + )} + /> setSaveLoading(false)); } diff --git a/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx b/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx index bb3b1c6e..18a54497 100644 --- a/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx +++ b/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx @@ -59,11 +59,6 @@ const accountFormSchema = z.object({ type AccountFormValues = z.infer; -const defaultValues: Partial = { - subdomain: "", - name: "My Resource", -}; - type CreateResourceFormProps = { open: boolean; setOpen: (open: boolean) => void; @@ -88,7 +83,10 @@ export default function CreateResourceForm({ const form = useForm({ resolver: zodResolver(accountFormSchema), - defaultValues, + defaultValues: { + subdomain: "", + name: "My Resource", + }, }); useEffect(() => { @@ -98,9 +96,13 @@ export default function CreateResourceForm({ const fetchSites = async () => { const res = await api.get>( - `/org/${orgId}/sites/` + `/org/${orgId}/sites/`, ); setSites(res.data.data.sites); + + if (res.data.data.sites.length > 0) { + form.setValue("siteId", res.data.data.sites[0].siteId); + } }; fetchSites(); @@ -116,7 +118,7 @@ export default function CreateResourceForm({ name: data.name, subdomain: data.subdomain, // subdomain: data.subdomain, - } + }, ) .catch((e) => { toast({ @@ -124,7 +126,7 @@ export default function CreateResourceForm({ title: "Error creating resource", description: formatAxiosError( e, - "An error occurred when creating the resource" + "An error occurred when creating the resource", ), }); }); @@ -196,7 +198,7 @@ export default function CreateResourceForm({ onChange={(value) => form.setValue( "subdomain", - value + value, ) } /> @@ -225,14 +227,14 @@ export default function CreateResourceForm({ className={cn( "w-[350px] justify-between", !field.value && - "text-muted-foreground" + "text-muted-foreground", )} > {field.value ? sites.find( (site) => site.siteId === - field.value + field.value, )?.name : "Select site"} @@ -259,7 +261,7 @@ export default function CreateResourceForm({ onSelect={() => { form.setValue( "siteId", - site.siteId + site.siteId, ); }} > @@ -269,14 +271,14 @@ export default function CreateResourceForm({ site.siteId === field.value ? "opacity-100" - : "opacity-0" + : "opacity-0", )} /> { site.name } - ) + ), )} diff --git a/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx index 2d065b24..2ffad8c1 100644 --- a/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx +++ b/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx @@ -24,7 +24,7 @@ import { Button } from "@app/components/ui/button"; import { useState } from "react"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "@app/components/DataTablePagination"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; interface ResourcesDataTableProps { columns: ColumnDef[]; @@ -62,19 +62,23 @@ export function ResourcesDataTable({ return (
- - table - .getColumn("name") - ?.setFilterValue(event.target.value) - } - className="max-w-sm mr-2" - /> +
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="w-full pl-8" + /> + +
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -98,7 +102,7 @@ export function ResourcesDataTable({ : flexRender( header.column.columnDef .header, - header.getContext() + header.getContext(), )} ); @@ -119,7 +123,7 @@ export function ResourcesDataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} diff --git a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx index 19208d1b..e8dfc643 100644 --- a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx @@ -9,7 +9,16 @@ import { DropdownMenuTrigger, } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { + Copy, + ArrowRight, + ArrowUpDown, + MoreHorizontal, + Check, + ArrowUpRight, + ShieldOff, + ShieldCheck, +} from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import api from "@app/api"; @@ -26,6 +35,8 @@ export type ResourceRow = { orgId: string; domain: string; site: string; + siteId: string; + hasAuth: boolean; }; type ResourcesTableProps = { @@ -91,10 +102,111 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { ); }, + cell: ({ row }) => { + const resourceRow = row.original; + return ( + + ); + }, }, { accessorKey: "domain", - header: "Domain", + header: "Full URL", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ + {resourceRow.domain} + + +
+ ); + }, + }, + { + accessorKey: "hasAuth", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ {resourceRow.hasAuth ? ( + + + Protected + + ) : ( + + + Not Protected + + )} +
+ ); + }, }, { id: "actions", @@ -130,28 +242,25 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { - + + ); diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index fdd30487..42cf449e 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -19,7 +19,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) { try { const res = await internal.get>( `/org/${params.orgId}/resources`, - await authCookieHeader() + await authCookieHeader(), ); resources = res.data.data.resources; } catch (e) { @@ -31,8 +31,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) { const getOrg = cache(async () => internal.get>( `/org/${params.orgId}`, - await authCookieHeader() - ) + await authCookieHeader(), + ), ); const res = await getOrg(); org = res.data.data; @@ -49,8 +49,13 @@ export default async function ResourcesPage(props: ResourcesPageProps) { id: resource.resourceId, name: resource.name, orgId: params.orgId, - domain: resource.subdomain || "", + domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, site: resource.siteName || "None", + siteId: resource.siteId || "Unknown", + hasAuth: + resource.sso || + resource.pincodeId !== null || + resource.pincodeId !== null, }; }); diff --git a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx index cb4fc120..c538e3b9 100644 --- a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx @@ -24,7 +24,7 @@ import { Button } from "@app/components/ui/button"; import { useState } from "react"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "../../../../../components/DataTablePagination"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; interface DataTableProps { columns: ColumnDef[]; @@ -62,19 +62,23 @@ export function SitesDataTable({ return (
- - table - .getColumn("name") - ?.setFilterValue(event.target.value) - } - className="max-w-sm mr-2" - /> +
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="w-full pl-8" + /> + +
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -98,7 +102,7 @@ export function SitesDataTable({ : flexRender( header.column.columnDef .header, - header.getContext() + header.getContext(), )} ); @@ -119,7 +123,7 @@ export function SitesDataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} diff --git a/src/app/[orgId]/settings/sites/components/SitesTable.tsx b/src/app/[orgId]/settings/sites/components/SitesTable.tsx index 124f952e..1ab69278 100644 --- a/src/app/[orgId]/settings/sites/components/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesTable.tsx @@ -24,9 +24,10 @@ export type SiteRow = { id: number; nice: string; name: string; - mbIn: number; - mbOut: number; + mbIn: string; + mbOut: string; orgId: string; + type: "newt" | "wireguard"; }; type SitesTableProps = { @@ -99,11 +100,70 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { }, { accessorKey: "mbIn", - header: "MB In", + header: ({ column }) => { + return ( + + ); + }, }, { accessorKey: "mbOut", - header: "MB Out", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "type", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const originalRow = row.original; + + if (originalRow.type === "newt") { + return ( +
+ Newt +
+ ); + } + + if (originalRow.type === "wireguard") { + return ( +
+ WireGuard +
+ ); + } + }, }, { id: "actions", @@ -135,24 +195,21 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { setSelectedSite(siteRow); setIsDeleteModalOpen(true); }} - className="text-red-600 hover:text-red-800" + className="text-red-500" > Delete - + + ); }, diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index fc758e92..63c1d7b5 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -15,21 +15,32 @@ export default async function SitesPage(props: SitesPageProps) { try { const res = await internal.get>( `/org/${params.orgId}/sites`, - await authCookieHeader() + await authCookieHeader(), ); sites = res.data.data.sites; } catch (e) { console.error("Error fetching sites", e); } + function formatSize(mb: number): string { + if (mb >= 1024 * 1024) { + return `${(mb / (1024 * 1024)).toFixed(2)} TB`; + } else if (mb >= 1024) { + return `${(mb / 1024).toFixed(2)} GB`; + } else { + return `${mb.toFixed(2)} MB`; + } + } + const siteRows: SiteRow[] = sites.map((site) => { return { name: site.name, id: site.siteId, nice: site.niceId.toString(), - mbIn: site.megabytesIn || 0, - mbOut: site.megabytesOut || 0, + mbIn: formatSize(site.megabytesIn || 0), + mbOut: formatSize(site.megabytesOut || 0), orgId: params.orgId, + type: site.type as any, }; }); @@ -37,7 +48,7 @@ export default async function SitesPage(props: SitesPageProps) { <> diff --git a/src/app/auth/login/DashboardLoginForm.tsx b/src/app/auth/login/DashboardLoginForm.tsx index 986993cd..61abe8cc 100644 --- a/src/app/auth/login/DashboardLoginForm.tsx +++ b/src/app/auth/login/DashboardLoginForm.tsx @@ -30,7 +30,15 @@ export default function DashboardLoginForm({ router.push("/")} + onLogin={() => { + if (redirect && redirect.includes("http")) { + window.location.href = redirect; + } else if (redirect) { + router.push(redirect); + } else { + router.push("/"); + } + }} /> diff --git a/src/app/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx index c352bb4d..b47b976f 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; import { Card, CardContent, @@ -5,8 +8,9 @@ import { CardHeader, CardTitle, } from "@app/components/ui/card"; +import Link from "next/link"; -export default async function ResourceAccessDenied() { +export default function ResourceAccessDenied() { return ( @@ -17,6 +21,11 @@ export default async function ResourceAccessDenied() { You're not alowed to access this resource. If this is a mistake, please contact the administrator. +
+ +
); diff --git a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx index a3277f63..014c8503 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -23,7 +23,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { LockIcon, UserIcon, Binary, Key, User } from "lucide-react"; +import { LockIcon, Binary, Key, User } from "lucide-react"; import { InputOTP, InputOTPGroup, @@ -34,10 +34,10 @@ import { useRouter } from "next/navigation"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { formatAxiosError } from "@app/lib/utils"; import { AxiosResponse } from "axios"; -import { LoginResponse } from "@server/routers/auth"; -import ResourceAccessDenied from "./ResourceAccessDenied"; import LoginForm from "@app/components/LoginForm"; import { AuthWithPasswordResponse } from "@server/routers/resource"; +import { redirect } from "next/dist/server/api-utils"; +import ResourceAccessDenied from "./ResourceAccessDenied"; const pinSchema = z.object({ pin: z @@ -63,7 +63,6 @@ type ResourceAuthPortalProps = { id: number; }; redirect: string; - queryParamName: string; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { @@ -114,13 +113,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { }, }); - function constructRedirect(redirect: string, token: string): string { - const redirectUrl = new URL(redirect); - redirectUrl.searchParams.delete(props.queryParamName); - redirectUrl.searchParams.append(props.queryParamName, token); - return redirectUrl.toString(); - } - const onPinSubmit = (values: z.infer) => { setLoadingLogin(true); api.post>( @@ -130,10 +122,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { .then((res) => { const session = res.data.data.session; if (session) { - window.location.href = constructRedirect( - props.redirect, - session, - ); + window.location.href = props.redirect; } }) .catch((e) => { @@ -156,10 +145,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { .then((res) => { const session = res.data.data.session; if (session) { - window.location.href = constructRedirect( - props.redirect, - session, - ); + window.location.href = props.redirect; } }) .catch((e) => { @@ -172,13 +158,15 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { }; async function handleSSOAuth() { - console.log("SSO authentication"); - - await api.get(`/resource/${props.resource.id}`).catch((e) => { + let isAllowed = false; + try { + await api.get(`/resource/${props.resource.id}`); + isAllowed = true; + } catch (e) { setAccessDenied(true); - }); + } - if (!accessDenied) { + if (isAllowed) { window.location.href = props.redirect; } } @@ -187,6 +175,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
{!accessDenied ? (
+
+ + Powered by Fossorial + +
Authentication Required @@ -378,6 +371,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { className={`${numMethods <= 1 ? "mt-0" : ""}`} > await handleSSOAuth() } diff --git a/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx b/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx index d72f5633..5b101297 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx @@ -1,3 +1,4 @@ +import { Button } from "@app/components/ui/button"; import { Card, CardContent, @@ -5,6 +6,7 @@ import { CardHeader, CardTitle, } from "@app/components/ui/card"; +import Link from "next/link"; export default async function ResourceNotFound() { return ( @@ -15,7 +17,12 @@ export default async function ResourceNotFound() { - The resource you're trying to access does not exist + The resource you're trying to access does not exist. +
+ +
); diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 32b7d067..23b27c43 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -3,7 +3,7 @@ import { GetResourceResponse, } from "@server/routers/resource"; import ResourceAuthPortal from "./components/ResourceAuthPortal"; -import { internal } from "@app/api"; +import { internal, priv } from "@app/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/api/cookies"; import { cache } from "react"; @@ -11,10 +11,12 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import ResourceNotFound from "./components/ResourceNotFound"; import ResourceAccessDenied from "./components/ResourceAccessDenied"; +import { cookies } from "next/headers"; +import { CheckResourceSessionResponse } from "@server/routers/auth"; export default async function ResourceAuthPage(props: { params: Promise<{ resourceId: number }>; - searchParams: Promise<{ redirect: string }>; + searchParams: Promise<{ redirect: string | undefined }>; }) { const params = await props.params; const searchParams = await props.searchParams; @@ -31,7 +33,7 @@ export default async function ResourceAuthPage(props: { } catch (e) {} const getUser = cache(verifySession); - const user = await getUser(); + const user = await getUser({ skipCheckVerifyEmail: true }); if (!authInfo) { return ( @@ -46,6 +48,40 @@ export default async function ResourceAuthPage(props: { const redirectUrl = searchParams.redirect || authInfo.url; + if ( + user && + !user.emailVerified && + process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true" + ) { + redirect( + `/auth/verify-email?redirect=/auth/resource/${authInfo.resourceId}`, + ); + } + + const allCookies = await cookies(); + const cookieName = + process.env.RESOURCE_SESSION_COOKIE_NAME + `_${params.resourceId}`; + const sessionId = allCookies.get(cookieName)?.value ?? null; + + if (sessionId) { + let doRedirect = false; + try { + const res = await priv.get< + AxiosResponse + >(`/resource-session/${params.resourceId}/${sessionId}`); + + console.log("resource session already exists and is valid"); + + if (res && res.data.data.valid) { + doRedirect = true; + } + } catch (e) {} + + if (doRedirect) { + redirect(redirectUrl); + } + } + if (!hasAuth) { // no authentication so always go straight to the resource redirect(redirectUrl); @@ -63,7 +99,6 @@ export default async function ResourceAuthPage(props: { console.log(res.data); doRedirect = true; } catch (e) { - console.error(e); userIsUnauthorized = true; } @@ -72,33 +107,28 @@ export default async function ResourceAuthPage(props: { } } - if (userIsUnauthorized && isSSOOnly) { - return ( -
- -
- ); - } - return ( <> -
- -
+ {userIsUnauthorized && isSSOOnly ? ( +
+ +
+ ) : ( +
+ +
+ )} ); } diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx index a8cedf9c..cceddc66 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -79,6 +79,7 @@ export default function VerifyEmailForm({ .catch((e) => { setError(formatAxiosError(e, "An error occurred")); console.error("Failed to verify email:", e); + setIsSubmitting(false); }); if (res && res.data?.data?.valid) { @@ -125,7 +126,7 @@ export default function VerifyEmailForm({
- Verify Your Email + Verify Email Enter the verification code sent to your email address. @@ -234,7 +235,7 @@ export default function VerifyEmailForm({ -
+