mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-05 18:26:40 +00:00
Merge branch 'main' of https://github.com/fosrl/pangolin
This commit is contained in:
@@ -1,27 +1,25 @@
|
|||||||
app:
|
app:
|
||||||
base_url: http://localhost:3000
|
base_url: https://fossorial.io
|
||||||
log_level: debug
|
log_level: debug
|
||||||
save_logs: false
|
save_logs: false
|
||||||
|
|
||||||
server:
|
server:
|
||||||
external_port: 3000
|
external_port: 3000
|
||||||
internal_port: 3001
|
internal_port: 3001
|
||||||
internal_hostname: localhost
|
internal_hostname: pangolin
|
||||||
secure_cookies: false
|
secure_cookies: false
|
||||||
session_cookie_name: session
|
session_cookie_name: session
|
||||||
|
resource_session_cookie_name: resource_session
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
cert_resolver: letsencrypt
|
cert_resolver: letsencrypt
|
||||||
http_entrypoint: web
|
http_entrypoint: web
|
||||||
https_entrypoint: websecure
|
https_entrypoint: websecure
|
||||||
|
prefer_wildcard_cert: true
|
||||||
badger:
|
|
||||||
session_query_parameter: __pang_sess
|
|
||||||
resource_session_cookie_name: resource_session
|
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
start_port: 51820
|
start_port: 51820
|
||||||
base_endpoint: 127.0.0.1
|
base_endpoint: fossorial.io
|
||||||
use_subdomain: false
|
use_subdomain: false
|
||||||
block_size: 16
|
block_size: 16
|
||||||
subnet_group: 10.0.0.0/8
|
subnet_group: 10.0.0.0/8
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"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:generate": "drizzle-kit generate",
|
||||||
"db:push": "npx tsx server/db/migrate.ts",
|
"db:push": "npx tsx server/db/migrate.ts",
|
||||||
"db:hydrate": "npx tsx scripts/hydrate.ts",
|
"db:hydrate": "npx tsx scripts/hydrate.ts",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"build": "mkdir -p dist && next build && node scripts/esbuild.mjs -e server/index.ts -o dist/server.mjs",
|
"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"
|
"email": "email dev --dir server/emails/templates --port 3002"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import { sha256 } from "@oslojs/crypto/sha2";
|
|||||||
import { resourceSessions, ResourceSession } from "@server/db/schema";
|
import { resourceSessions, ResourceSession } from "@server/db/schema";
|
||||||
import db from "@server/db";
|
import db from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import config from "@server/config";
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME = "resource_session";
|
export const SESSION_COOKIE_NAME = "resource_session";
|
||||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
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: {
|
export async function createResourceSession(opts: {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -115,25 +119,25 @@ export async function invalidateAllSessions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function serializeResourceSessionCookie(
|
export function serializeResourceSessionCookie(
|
||||||
|
cookieName: string,
|
||||||
token: string,
|
token: string,
|
||||||
fqdn: string,
|
fqdn: string,
|
||||||
secure: boolean,
|
|
||||||
): string {
|
): string {
|
||||||
if (secure) {
|
if (SECURE_COOKIES) {
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=.localhost`;
|
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
} else {
|
} 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(
|
export function createBlankResourceSessionTokenCookie(
|
||||||
|
cookieName: string,
|
||||||
fqdn: string,
|
fqdn: string,
|
||||||
secure: boolean,
|
|
||||||
): string {
|
): string {
|
||||||
if (secure) {
|
if (SECURE_COOKIES) {
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${fqdn}`;
|
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
} else {
|
} 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(),
|
secure_cookies: z.boolean(),
|
||||||
signup_secret: z.string().optional(),
|
signup_secret: z.string().optional(),
|
||||||
session_cookie_name: z.string(),
|
session_cookie_name: z.string(),
|
||||||
}),
|
|
||||||
badger: z.object({
|
|
||||||
session_query_parameter: z.string(),
|
|
||||||
resource_session_cookie_name: z.string(),
|
resource_session_cookie_name: z.string(),
|
||||||
}),
|
}),
|
||||||
traefik: z.object({
|
traefik: z.object({
|
||||||
@@ -128,12 +125,14 @@ if (!parsedConfig.success) {
|
|||||||
|
|
||||||
process.env.SERVER_EXTERNAL_PORT =
|
process.env.SERVER_EXTERNAL_PORT =
|
||||||
parsedConfig.data.server.external_port.toString();
|
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
|
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
|
||||||
?.require_email_verification
|
?.require_email_verification
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name;
|
process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name;
|
||||||
process.env.RESOURCE_SESSION_QUERY_PARAM_NAME =
|
process.env.RESOURCE_SESSION_COOKIE_NAME =
|
||||||
parsedConfig.data.badger.session_query_parameter;
|
parsedConfig.data.server.resource_session_cookie_name;
|
||||||
|
|
||||||
export default parsedConfig.data;
|
export default parsedConfig.data;
|
||||||
|
|||||||
@@ -287,16 +287,28 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
|||||||
() => resourcePassword.passwordId,
|
() => resourcePassword.passwordId,
|
||||||
{
|
{
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
pincodeId: integer("pincodeId").references(
|
pincodeId: integer("pincodeId").references(
|
||||||
() => resourcePincode.pincodeId,
|
() => resourcePincode.pincodeId,
|
||||||
{
|
{
|
||||||
onDelete: "cascade",
|
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 Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -325,3 +337,4 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
|||||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
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 { ReactElement } from "react";
|
||||||
import emailClient from "@server/emails";
|
import emailClient from "@server/emails";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|||||||
@@ -33,9 +33,17 @@ export const SendInviteLink = ({
|
|||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
<Preview>{previewText}</Preview>
|
<Preview>{previewText}</Preview>
|
||||||
<Tailwind>
|
<Tailwind config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: "#F97317"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<Body className="font-sans">
|
<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">
|
<Heading className="text-2xl font-semibold text-gray-800 text-center">
|
||||||
You're invited to join a Fossorial organization
|
You're invited to join a Fossorial organization
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -58,7 +66,7 @@ export const SendInviteLink = ({
|
|||||||
<Section className="text-center my-6">
|
<Section className="text-center my-6">
|
||||||
<Button
|
<Button
|
||||||
href={inviteLink}
|
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}
|
Accept invitation to {orgName}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import * as React from "react";
|
|||||||
interface VerifyEmailProps {
|
interface VerifyEmailProps {
|
||||||
username?: string;
|
username?: string;
|
||||||
verificationCode: string;
|
verificationCode: string;
|
||||||
|
verifyLink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VerifyEmail = ({
|
export const VerifyEmail = ({
|
||||||
username,
|
username,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
|
verifyLink,
|
||||||
}: VerifyEmailProps) => {
|
}: VerifyEmailProps) => {
|
||||||
const previewText = `Verify your email, ${username}`;
|
const previewText = `Verify your email, ${username}`;
|
||||||
|
|
||||||
@@ -26,21 +28,34 @@ export const VerifyEmail = ({
|
|||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
<Preview>{previewText}</Preview>
|
<Preview>{previewText}</Preview>
|
||||||
<Tailwind>
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: "#F97317",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Body className="font-sans">
|
<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">
|
<Heading className="text-2xl font-semibold text-gray-800 text-center">
|
||||||
Verify Your Email
|
Please verify your email
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text className="text-base text-gray-700 mt-4">
|
<Text className="text-base text-gray-700 mt-4">
|
||||||
Hi {username || "there"},
|
Hi {username || "there"},
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-base text-gray-700 mt-2">
|
<Text className="text-base text-gray-700 mt-2">
|
||||||
You’ve requested to verify your email. Please use
|
You’ve requested to verify your email. Please{" "}
|
||||||
the verification code below:
|
<a href={verifyLink} className="text-primary">
|
||||||
|
click here
|
||||||
|
</a>{" "}
|
||||||
|
to verify your email, then enter the following code:
|
||||||
</Text>
|
</Text>
|
||||||
<Section className="text-center my-6">
|
<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}
|
{verificationCode}
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -59,3 +74,5 @@ export const VerifyEmail = ({
|
|||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default VerifyEmail;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ app.prepare().then(() => {
|
|||||||
externalServer.use(logIncomingMiddleware);
|
externalServer.use(logIncomingMiddleware);
|
||||||
externalServer.use(prefix, unauthenticated);
|
externalServer.use(prefix, unauthenticated);
|
||||||
externalServer.use(prefix, authenticated);
|
externalServer.use(prefix, authenticated);
|
||||||
externalServer.use(`${prefix}/ws`, wsRouter);
|
// externalServer.use(`${prefix}/ws`, wsRouter);
|
||||||
|
|
||||||
externalServer.use(notFoundMiddleware);
|
externalServer.use(notFoundMiddleware);
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ app.prepare().then(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
handleWSUpgrade(httpServer);
|
// handleWSUpgrade(httpServer);
|
||||||
|
|
||||||
externalServer.use(errorHandlerMiddleware);
|
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 "./changePassword";
|
||||||
export * from "./requestPasswordReset";
|
export * from "./requestPasswordReset";
|
||||||
export * from "./resetPassword";
|
export * from "./resetPassword";
|
||||||
|
export * from "./checkResourceSession";
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export async function login(
|
|||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Email verification code sent",
|
message: "Email verification code sent",
|
||||||
status: HttpCode.ACCEPTED,
|
status: HttpCode.OK,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,18 @@ export async function sendEmailVerificationCode(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const code = await generateEmailVerificationCode(userId, email);
|
const code = await generateEmailVerificationCode(userId, email);
|
||||||
|
|
||||||
await sendEmail(VerifyEmail({ username: email, verificationCode: code }), {
|
await sendEmail(
|
||||||
to: email,
|
VerifyEmail({
|
||||||
from: config.email?.no_reply,
|
username: email,
|
||||||
subject: "Verify your email address",
|
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(
|
async function generateEmailVerificationCode(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
resourcePassword,
|
resourcePassword,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resources,
|
resources,
|
||||||
|
User,
|
||||||
userOrgs,
|
userOrgs,
|
||||||
} from "@server/db/schema";
|
} from "@server/db/schema";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
@@ -19,10 +20,7 @@ import { Resource, roleResources, userResources } from "@server/db/schema";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
const verifyResourceSessionSchema = z.object({
|
const verifyResourceSessionSchema = z.object({
|
||||||
sessions: z.object({
|
sessions: z.record(z.string()).optional(),
|
||||||
session: z.string().nullable(),
|
|
||||||
resource_session: z.string().nullable(),
|
|
||||||
}),
|
|
||||||
originalRequestURL: z.string().url(),
|
originalRequestURL: z.string().url(),
|
||||||
scheme: z.string(),
|
scheme: z.string(),
|
||||||
host: 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)}`;
|
const redirectUrl = `${config.app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||||
|
|
||||||
if (sso && sessions.session) {
|
if (!sessions) {
|
||||||
const { session, user } = await validateSessionToken(
|
return notAllowed(res);
|
||||||
sessions.session,
|
}
|
||||||
);
|
|
||||||
|
const sessionToken = sessions[config.server.session_cookie_name];
|
||||||
|
|
||||||
|
// check for unified login
|
||||||
|
if (sso && sessionToken) {
|
||||||
|
const { session, user } = await validateSessionToken(sessionToken);
|
||||||
if (session && user) {
|
if (session && user) {
|
||||||
const isAllowed = await isUserAllowedToAccessResource(
|
const isAllowed = await isUserAllowedToAccessResource(
|
||||||
user.userId,
|
user,
|
||||||
resource,
|
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(
|
const { resourceSession } = await validateResourceSessionToken(
|
||||||
sessions.resource_session,
|
resourceSessionToken,
|
||||||
resource.resourceId,
|
resource.resourceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resourceSession) {
|
if (resourceSession) {
|
||||||
if (
|
if (
|
||||||
pincode &&
|
pincode &&
|
||||||
@@ -165,7 +174,7 @@ function notAllowed(res: Response, redirectUrl?: string) {
|
|||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
}
|
};
|
||||||
logger.debug(JSON.stringify(data));
|
logger.debug(JSON.stringify(data));
|
||||||
return response<VerifyUserResponse>(res, data);
|
return response<VerifyUserResponse>(res, data);
|
||||||
}
|
}
|
||||||
@@ -177,21 +186,25 @@ function allowed(res: Response) {
|
|||||||
error: false,
|
error: false,
|
||||||
message: "Access allowed",
|
message: "Access allowed",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
}
|
};
|
||||||
logger.debug(JSON.stringify(data));
|
logger.debug(JSON.stringify(data));
|
||||||
return response<VerifyUserResponse>(res, data);
|
return response<VerifyUserResponse>(res, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isUserAllowedToAccessResource(
|
async function isUserAllowedToAccessResource(
|
||||||
userId: string,
|
user: User,
|
||||||
resource: Resource,
|
resource: Resource,
|
||||||
) {
|
): Promise<boolean> {
|
||||||
|
if (config.flags?.require_email_verification && !user.emailVerified) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const userOrgRole = await db
|
const userOrgRole = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(userOrgs.userId, userId),
|
eq(userOrgs.userId, user.userId),
|
||||||
eq(userOrgs.orgId, resource.orgId),
|
eq(userOrgs.orgId, resource.orgId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -221,7 +234,7 @@ async function isUserAllowedToAccessResource(
|
|||||||
.from(userResources)
|
.from(userResources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(userResources.userId, userId),
|
eq(userResources.userId, user.userId),
|
||||||
eq(userResources.resourceId, resource.resourceId),
|
eq(userResources.resourceId, resource.resourceId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router } from "express";
|
|||||||
import * as gerbil from "@server/routers/gerbil";
|
import * as gerbil from "@server/routers/gerbil";
|
||||||
import * as badger from "@server/routers/badger";
|
import * as badger from "@server/routers/badger";
|
||||||
import * as traefik from "@server/routers/traefik";
|
import * as traefik from "@server/routers/traefik";
|
||||||
|
import * as auth from "@server/routers/auth";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
@@ -12,6 +13,10 @@ internalRouter.get("/", (_, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
||||||
|
internalRouter.get(
|
||||||
|
"/resource-session/:resourceId/:token",
|
||||||
|
auth.checkResourceSession,
|
||||||
|
);
|
||||||
|
|
||||||
// Gerbil routes
|
// Gerbil routes
|
||||||
const gerbilRouter = Router();
|
const gerbilRouter = Router();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
serializeResourceSessionCookie,
|
serializeResourceSessionCookie,
|
||||||
} from "@server/auth/resource";
|
} from "@server/auth/resource";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import config from "@server/config";
|
||||||
|
|
||||||
export const authWithPasswordBodySchema = z.object({
|
export const authWithPasswordBodySchema = z.object({
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
@@ -131,15 +132,15 @@ export async function authWithPassword(
|
|||||||
token,
|
token,
|
||||||
passwordId: definedPassword.passwordId,
|
passwordId: definedPassword.passwordId,
|
||||||
});
|
});
|
||||||
// const secureCookie = resource.ssl;
|
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||||
// const cookie = serializeResourceSessionCookie(
|
const cookie = serializeResourceSessionCookie(
|
||||||
// token,
|
cookieName,
|
||||||
// resource.fullDomain,
|
token,
|
||||||
// secureCookie,
|
resource.fullDomain,
|
||||||
// );
|
);
|
||||||
// res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
|
||||||
// logger.debug(cookie); // remove after testing
|
logger.debug(cookie); // remove after testing
|
||||||
|
|
||||||
return response<AuthWithPasswordResponse>(res, {
|
return response<AuthWithPasswordResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
serializeResourceSessionCookie,
|
serializeResourceSessionCookie,
|
||||||
} from "@server/auth/resource";
|
} from "@server/auth/resource";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import config from "@server/config";
|
||||||
|
|
||||||
export const authWithPincodeBodySchema = z.object({
|
export const authWithPincodeBodySchema = z.object({
|
||||||
pincode: z.string(),
|
pincode: z.string(),
|
||||||
@@ -127,15 +128,15 @@ export async function authWithPincode(
|
|||||||
token,
|
token,
|
||||||
pincodeId: definedPincode.pincodeId,
|
pincodeId: definedPincode.pincodeId,
|
||||||
});
|
});
|
||||||
// const secureCookie = resource.ssl;
|
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||||
// const cookie = serializeResourceSessionCookie(
|
const cookie = serializeResourceSessionCookie(
|
||||||
// token,
|
cookieName,
|
||||||
// resource.fullDomain,
|
token,
|
||||||
// secureCookie,
|
resource.fullDomain,
|
||||||
// );
|
);
|
||||||
// res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
|
||||||
// logger.debug(cookie); // remove after testing
|
logger.debug(cookie); // remove after testing
|
||||||
|
|
||||||
return response<AuthWithPincodeResponse>(res, {
|
return response<AuthWithPincodeResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export async function createResource(
|
|||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subdomain,
|
subdomain,
|
||||||
|
ssl: true,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
sites,
|
sites,
|
||||||
userResources,
|
userResources,
|
||||||
roleResources,
|
roleResources,
|
||||||
|
resourcePassword,
|
||||||
|
resourcePincode,
|
||||||
} from "@server/db/schema";
|
} from "@server/db/schema";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -46,39 +48,65 @@ const listResourcesSchema = z.object({
|
|||||||
function queryResources(
|
function queryResources(
|
||||||
accessibleResourceIds: number[],
|
accessibleResourceIds: number[],
|
||||||
siteId?: number,
|
siteId?: number,
|
||||||
orgId?: string
|
orgId?: string,
|
||||||
) {
|
) {
|
||||||
if (siteId) {
|
if (siteId) {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
resourceId: resources.resourceId,
|
resourceId: resources.resourceId,
|
||||||
name: resources.name,
|
name: resources.name,
|
||||||
subdomain: resources.subdomain,
|
fullDomain: resources.fullDomain,
|
||||||
|
ssl: resources.ssl,
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
|
siteId: sites.niceId,
|
||||||
|
passwordId: resourcePassword.passwordId,
|
||||||
|
pincodeId: resourcePincode.pincodeId,
|
||||||
|
sso: resources.sso,
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||||
|
.leftJoin(
|
||||||
|
resourcePassword,
|
||||||
|
eq(resourcePassword.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePincode,
|
||||||
|
eq(resourcePincode.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(resources.resourceId, accessibleResourceIds),
|
inArray(resources.resourceId, accessibleResourceIds),
|
||||||
eq(resources.siteId, siteId)
|
eq(resources.siteId, siteId),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
} else if (orgId) {
|
} else if (orgId) {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
resourceId: resources.resourceId,
|
resourceId: resources.resourceId,
|
||||||
name: resources.name,
|
name: resources.name,
|
||||||
subdomain: resources.subdomain,
|
ssl: resources.ssl,
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
|
siteId: sites.niceId,
|
||||||
|
passwordId: resourcePassword.passwordId,
|
||||||
|
sso: resources.sso,
|
||||||
|
pincodeId: resourcePincode.pincodeId,
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||||
|
.leftJoin(
|
||||||
|
resourcePassword,
|
||||||
|
eq(resourcePassword.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePincode,
|
||||||
|
eq(resourcePincode.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(resources.resourceId, accessibleResourceIds),
|
inArray(resources.resourceId, accessibleResourceIds),
|
||||||
eq(resources.orgId, orgId)
|
eq(resources.orgId, orgId),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +119,7 @@ export type ListResourcesResponse = {
|
|||||||
export async function listResources(
|
export async function listResources(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedQuery = listResourcesSchema.safeParse(req.query);
|
const parsedQuery = listResourcesSchema.safeParse(req.query);
|
||||||
@@ -99,8 +127,8 @@ export async function listResources(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
parsedQuery.error.errors.map((e) => e.message).join(", ")
|
parsedQuery.error.errors.map((e) => e.message).join(", "),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { limit, offset } = parsedQuery.data;
|
||||||
@@ -110,8 +138,8 @@ export async function listResources(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
parsedParams.error.errors.map((e) => e.message).join(", ")
|
parsedParams.error.errors.map((e) => e.message).join(", "),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { siteId, orgId } = parsedParams.data;
|
const { siteId, orgId } = parsedParams.data;
|
||||||
@@ -120,8 +148,8 @@ export async function listResources(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have access to this organization"
|
"User does not have access to this organization",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,17 +160,17 @@ export async function listResources(
|
|||||||
.from(userResources)
|
.from(userResources)
|
||||||
.fullJoin(
|
.fullJoin(
|
||||||
roleResources,
|
roleResources,
|
||||||
eq(userResources.resourceId, roleResources.resourceId)
|
eq(userResources.resourceId, roleResources.resourceId),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
or(
|
or(
|
||||||
eq(userResources.userId, req.user!.userId),
|
eq(userResources.userId, req.user!.userId),
|
||||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
eq(roleResources.roleId, req.userOrgRoleId!),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const accessibleResourceIds = accessibleResources.map(
|
const accessibleResourceIds = accessibleResources.map(
|
||||||
(resource) => resource.resourceId
|
(resource) => resource.resourceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
let countQuery: any = db
|
let countQuery: any = db
|
||||||
@@ -173,7 +201,10 @@ export async function listResources(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return next(
|
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,
|
megabytesIn: sites.megabytesIn,
|
||||||
megabytesOut: sites.megabytesOut,
|
megabytesOut: sites.megabytesOut,
|
||||||
orgName: orgs.name,
|
orgName: orgs.name,
|
||||||
|
type: sites.type,
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.leftJoin(orgs, eq(sites.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(sites.orgId, orgs.orgId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(sites.siteId, accessibleSiteIds),
|
inArray(sites.siteId, accessibleSiteIds),
|
||||||
eq(sites.orgId, orgId)
|
eq(sites.orgId, orgId),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export type ListSitesResponse = {
|
|||||||
export async function listSites(
|
export async function listSites(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedQuery = listSitesSchema.safeParse(req.query);
|
const parsedQuery = listSitesSchema.safeParse(req.query);
|
||||||
@@ -65,8 +66,8 @@ export async function listSites(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
fromError(parsedQuery.error)
|
fromError(parsedQuery.error),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { limit, offset } = parsedQuery.data;
|
||||||
@@ -76,8 +77,8 @@ export async function listSites(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
fromError(parsedParams.error)
|
fromError(parsedParams.error),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
@@ -86,8 +87,8 @@ export async function listSites(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have access to this organization"
|
"User does not have access to this organization",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +101,8 @@ export async function listSites(
|
|||||||
.where(
|
.where(
|
||||||
or(
|
or(
|
||||||
eq(userSites.userId, req.user!.userId),
|
eq(userSites.userId, req.user!.userId),
|
||||||
eq(roleSites.roleId, req.userOrgRoleId!)
|
eq(roleSites.roleId, req.userOrgRoleId!),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
||||||
@@ -113,8 +114,8 @@ export async function listSites(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(sites.siteId, accessibleSiteIds),
|
inArray(sites.siteId, accessibleSiteIds),
|
||||||
eq(sites.orgId, orgId)
|
eq(sites.orgId, orgId),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sitesList = await baseQuery.limit(limit).offset(offset);
|
const sitesList = await baseQuery.limit(limit).offset(offset);
|
||||||
@@ -137,7 +138,10 @@ export async function listSites(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return next(
|
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,
|
schema.orgs,
|
||||||
eq(schema.resources.orgId, schema.orgs.orgId),
|
eq(schema.resources.orgId, schema.orgs.orgId),
|
||||||
)
|
)
|
||||||
|
.innerJoin(
|
||||||
|
schema.sites,
|
||||||
|
eq(schema.sites.siteId, schema.resources.siteId),
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(schema.targets.enabled, true),
|
eq(schema.targets.enabled, true),
|
||||||
@@ -51,11 +55,9 @@ export async function traefikConfigProvider(
|
|||||||
`http://${config.server.internal_hostname}:${config.server.internal_port}`,
|
`http://${config.server.internal_hostname}:${config.server.internal_port}`,
|
||||||
).href,
|
).href,
|
||||||
resourceSessionCookieName:
|
resourceSessionCookieName:
|
||||||
config.badger.resource_session_cookie_name,
|
config.server.resource_session_cookie_name,
|
||||||
userSessionCookieName:
|
userSessionCookieName:
|
||||||
config.server.session_cookie_name,
|
config.server.session_cookie_name,
|
||||||
sessionQueryParameter:
|
|
||||||
config.badger.session_query_parameter,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -70,6 +72,7 @@ export async function traefikConfigProvider(
|
|||||||
for (const item of all) {
|
for (const item of all) {
|
||||||
const target = item.targets;
|
const target = item.targets;
|
||||||
const resource = item.resources;
|
const resource = item.resources;
|
||||||
|
const site = item.sites;
|
||||||
const org = item.orgs;
|
const org = item.orgs;
|
||||||
|
|
||||||
const routerName = `${target.targetId}-router`;
|
const routerName = `${target.targetId}-router`;
|
||||||
@@ -112,7 +115,7 @@ export async function traefikConfigProvider(
|
|||||||
? config.traefik.https_entrypoint
|
? config.traefik.https_entrypoint
|
||||||
: config.traefik.http_entrypoint,
|
: config.traefik.http_entrypoint,
|
||||||
],
|
],
|
||||||
middlewares: resource.ssl ? [badgerMiddlewareName] : [],
|
middlewares: [badgerMiddlewareName],
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: `Host(\`${fullDomain}\`)`,
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
...(resource.ssl ? { tls } : {}),
|
...(resource.ssl ? { tls } : {}),
|
||||||
@@ -128,15 +131,28 @@ export async function traefikConfigProvider(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
http.services![serviceName] = {
|
if (site.type === "newt") {
|
||||||
loadBalancer: {
|
const ip = site.subnet.split("/")[0];
|
||||||
servers: [
|
http.services![serviceName] = {
|
||||||
{
|
loadBalancer: {
|
||||||
url: `${target.method}://${target.ip}:${target.port}`,
|
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 });
|
return res.status(HttpCode.OK).json({ http });
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export async function inviteUser(
|
|||||||
|
|
||||||
inviteTracker[email].timestamps.push(currentTime);
|
inviteTracker[email].timestamps.push(currentTime);
|
||||||
|
|
||||||
logger.debug("here0")
|
|
||||||
const org = await db
|
const org = await db
|
||||||
.select()
|
.select()
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
@@ -98,7 +97,6 @@ export async function inviteUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("here1")
|
|
||||||
const existingUser = await db
|
const existingUser = await db
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
@@ -114,7 +112,6 @@ export async function inviteUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("here2")
|
|
||||||
const inviteId = generateRandomString(
|
const inviteId = generateRandomString(
|
||||||
10,
|
10,
|
||||||
alphabet("a-z", "A-Z", "0-9"),
|
alphabet("a-z", "A-Z", "0-9"),
|
||||||
@@ -124,7 +121,6 @@ export async function inviteUser(
|
|||||||
|
|
||||||
const tokenHash = await hashPassword(token);
|
const tokenHash = await hashPassword(token);
|
||||||
|
|
||||||
logger.debug("here3")
|
|
||||||
// delete any existing invites for this email
|
// delete any existing invites for this email
|
||||||
await db
|
await db
|
||||||
.delete(userInvites)
|
.delete(userInvites)
|
||||||
@@ -133,7 +129,6 @@ export async function inviteUser(
|
|||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
logger.debug("here4")
|
|
||||||
await db.insert(userInvites).values({
|
await db.insert(userInvites).values({
|
||||||
inviteId,
|
inviteId,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -145,23 +140,21 @@ export async function inviteUser(
|
|||||||
|
|
||||||
const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`;
|
const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`;
|
||||||
|
|
||||||
logger.debug("here5")
|
await sendEmail(
|
||||||
// await sendEmail(
|
SendInviteLink({
|
||||||
// SendInviteLink({
|
email,
|
||||||
// email,
|
inviteLink,
|
||||||
// inviteLink,
|
expiresInDays: (validHours / 24).toString(),
|
||||||
// expiresInDays: (validHours / 24).toString(),
|
orgName: org[0].name || orgId,
|
||||||
// orgName: org[0].name || orgId,
|
inviterName: req.user?.email,
|
||||||
// inviterName: req.user?.email,
|
}),
|
||||||
// }),
|
{
|
||||||
// {
|
to: email,
|
||||||
// to: email,
|
from: config.email?.no_reply,
|
||||||
// from: config.email?.no_reply,
|
subject: "You're invited to join a Fossorial organization",
|
||||||
// subject: "You're invited to join a Fossorial organization",
|
},
|
||||||
// },
|
);
|
||||||
// );
|
|
||||||
|
|
||||||
logger.debug("here6")
|
|
||||||
return response<InviteUserResponse>(res, {
|
return response<InviteUserResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
inviteLink,
|
inviteLink,
|
||||||
|
|||||||
@@ -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;
|
export default api;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { internal } from "@app/api";
|
import { internal } from "@app/api";
|
||||||
import { authCookieHeader } from "@app/api/cookies";
|
import { authCookieHeader } from "@app/api/cookies";
|
||||||
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import { GetOrgResponse } from "@server/routers/org";
|
import { GetOrgResponse } from "@server/routers/org";
|
||||||
|
import { GetOrgUserResponse } from "@server/routers/user";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
@@ -17,6 +19,25 @@ export default async function OrgLayout(props: {
|
|||||||
redirect(`/`);
|
redirect(`/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUser = cache(verifySession);
|
||||||
|
const user = await getUser();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
redirect(`/?redirect=/${orgId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const getOrgUser = cache(() =>
|
||||||
|
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||||
|
`/org/${orgId}/user/${user.userId}`,
|
||||||
|
cookie,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const orgUser = await getOrgUser();
|
||||||
|
} catch {
|
||||||
|
redirect(`/`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const getOrg = cache(() =>
|
const getOrg = cache(() =>
|
||||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||||
|
|||||||
@@ -14,27 +14,6 @@ export default async function OrgPage(props: OrgPageProps) {
|
|||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const orgId = params.orgId;
|
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<AxiosResponse<GetOrgUserResponse>>(
|
|
||||||
`/org/${orgId}/user/${user.userId}`,
|
|
||||||
cookie
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const orgUser = await getOrgUser();
|
|
||||||
} catch {
|
|
||||||
redirect(`/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>Welcome to {orgId} dashboard</p>
|
<p>Welcome to {orgId} dashboard</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
import { SidebarSettings } from "@app/components/SidebarSettings";
|
||||||
|
|
||||||
type AccessPageHeaderAndNavProps = {
|
type AccessPageHeaderAndNavProps = {
|
||||||
@@ -22,16 +23,12 @@ export default function AccessPageHeaderAndNav({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{" "}
|
<SettingsSectionTitle
|
||||||
<div className="space-y-0.5 select-none mb-6">
|
title="Manage Users & Roles"
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
description="Invite users and add them to roles to manage access to your
|
||||||
Users & Roles
|
organization"
|
||||||
</h2>
|
/>
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Invite users and add them to roles to manage access to your
|
|
||||||
organization
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||||
{children}
|
{children}
|
||||||
</SidebarSettings>
|
</SidebarSettings>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus, Search } from "lucide-react";
|
||||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
@@ -61,19 +61,23 @@ export function RolesDataTable<TData, TValue>({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between pb-4">
|
<div className="flex items-center justify-between pb-4">
|
||||||
<Input
|
<div className="flex items-center max-w-sm mr-2 w-full relative">
|
||||||
placeholder="Search roles"
|
<Input
|
||||||
value={
|
placeholder="Search roles"
|
||||||
(table.getColumn("name")?.getFilterValue() as string) ??
|
value={
|
||||||
""
|
(table
|
||||||
}
|
.getColumn("name")
|
||||||
onChange={(event) =>
|
?.getFilterValue() as string) ?? ""
|
||||||
table
|
}
|
||||||
.getColumn("name")
|
onChange={(event) =>
|
||||||
?.setFilterValue(event.target.value)
|
table
|
||||||
}
|
.getColumn("name")
|
||||||
className="max-w-sm mr-2"
|
?.setFilterValue(event.target.value)
|
||||||
/>
|
}
|
||||||
|
className="w-full pl-8"
|
||||||
|
/>
|
||||||
|
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (addRole) {
|
if (addRole) {
|
||||||
@@ -84,7 +88,7 @@ export function RolesDataTable<TData, TValue>({
|
|||||||
<Plus className="mr-2 h-4 w-4" /> Add Role
|
<Plus className="mr-2 h-4 w-4" /> Add Role
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="border rounded-md">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -97,7 +101,7 @@ export function RolesDataTable<TData, TValue>({
|
|||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef
|
header.column.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext()
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
@@ -118,7 +122,7 @@ export function RolesDataTable<TData, TValue>({
|
|||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext()
|
cell.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
|
{roleRow.isAdmin && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0 opacity-0 cursor-default"
|
||||||
|
>
|
||||||
|
Placeholder
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{!roleRow.isAdmin && (
|
{!roleRow.isAdmin && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -81,7 +89,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<button
|
<button
|
||||||
className="text-red-600 hover:text-red-800"
|
className="text-red-500"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setUserToRemove(roleRow);
|
setUserToRemove(roleRow);
|
||||||
@@ -117,7 +125,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
roleToDelete={roleToRemove}
|
roleToDelete={roleToRemove}
|
||||||
afterDelete={() => {
|
afterDelete={() => {
|
||||||
setRoles((prev) =>
|
setRoles((prev) =>
|
||||||
prev.filter((r) => r.roleId !== roleToRemove.roleId)
|
prev.filter(
|
||||||
|
(r) => r.roleId !== roleToRemove.roleId,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
setUserToRemove(null);
|
setUserToRemove(null);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { Button } from "@app/components/ui/button";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { DataTablePagination } from "../../../../../../components/DataTablePagination";
|
import { DataTablePagination } from "../../../../../../components/DataTablePagination";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus, Search } from "lucide-react";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
@@ -61,20 +61,23 @@ export function UsersDataTable<TData, TValue>({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between pb-4">
|
<div className="flex items-center justify-between pb-4">
|
||||||
<Input
|
<div className="flex items-center max-w-sm mr-2 w-full relative">
|
||||||
placeholder="Search users"
|
<Input
|
||||||
value={
|
placeholder="Search users"
|
||||||
(table
|
value={
|
||||||
.getColumn("email")
|
(table
|
||||||
?.getFilterValue() as string) ?? ""
|
.getColumn("email")
|
||||||
}
|
?.getFilterValue() as string) ?? ""
|
||||||
onChange={(event) =>
|
}
|
||||||
table
|
onChange={(event) =>
|
||||||
.getColumn("email")
|
table
|
||||||
?.setFilterValue(event.target.value)
|
.getColumn("email")
|
||||||
}
|
?.setFilterValue(event.target.value)
|
||||||
className="max-w-sm mr-2"
|
}
|
||||||
/>
|
className="w-full pl-8"
|
||||||
|
/>
|
||||||
|
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (inviteUser) {
|
if (inviteUser) {
|
||||||
@@ -85,7 +88,7 @@ export function UsersDataTable<TData, TValue>({
|
|||||||
<Plus className="mr-2 h-4 w-4" /> Invite User
|
<Plus className="mr-2 h-4 w-4" /> Invite User
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="border rounded-md">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -98,7 +101,7 @@ export function UsersDataTable<TData, TValue>({
|
|||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef
|
header.column.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext()
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
@@ -119,7 +122,7 @@ export function UsersDataTable<TData, TValue>({
|
|||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext()
|
cell.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center gap-1">
|
<div className="flex flex-row items-center gap-1">
|
||||||
{userRow.isOwner && <Crown className="w-4 h-4" />}
|
{userRow.isOwner && (
|
||||||
|
<Crown className="w-4 h-4 text-yellow-600" />
|
||||||
|
)}
|
||||||
<span>{userRow.role}</span>
|
<span>{userRow.role}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -113,6 +115,14 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
|
{userRow.isOwner && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="opacity-0 cursor-default"
|
||||||
|
>
|
||||||
|
Placeholder
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{!userRow.isOwner && (
|
{!userRow.isOwner && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -138,13 +148,13 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
{userRow.email !== user?.email && (
|
{userRow.email !== user?.email && (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<button
|
<button
|
||||||
className="text-red-600 hover:text-red-800"
|
className="text-red-500"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(
|
setIsDeleteModalOpen(
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
setSelectedUser(
|
setSelectedUser(
|
||||||
userRow
|
userRow,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -154,18 +164,17 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Button
|
<Link
|
||||||
variant={"gray"}
|
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||||
className="ml-2"
|
|
||||||
onClick={() =>
|
|
||||||
router.push(
|
|
||||||
`/${org?.org.orgId}/settings/access/users/${userRow.id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Manage{" "}
|
<Button
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
variant={"gray"}
|
||||||
</Button>
|
className="ml-2"
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -185,7 +194,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
title: "Failed to remove user",
|
title: "Failed to remove user",
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
"An error occurred while removing the user."
|
"An error occurred while removing the user.",
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -198,7 +207,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setUsers((prev) =>
|
setUsers((prev) =>
|
||||||
prev.filter((u) => u.id !== selectedUser?.id)
|
prev.filter((u) => u.id !== selectedUser?.id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
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"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
@@ -202,29 +202,6 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
|
|||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
{/* <Select
|
|
||||||
defaultValue={orgId}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
router.push(`/${val}/settings`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[100px] md:w-[180px]">
|
|
||||||
<SelectValue placeholder="Select an org" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
{orgs.map((org) => (
|
|
||||||
<SelectItem
|
|
||||||
value={org.name}
|
|
||||||
key={org.orgId}
|
|
||||||
>
|
|
||||||
{org.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/auth/login");
|
redirect(`/?redirect=/${orgId}/settings/general`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let orgUser = null;
|
let orgUser = null;
|
||||||
@@ -34,8 +34,8 @@ export default async function GeneralSettingsPage({
|
|||||||
const getOrgUser = cache(async () =>
|
const getOrgUser = cache(async () =>
|
||||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||||
`/org/${orgId}/user/${user.userId}`,
|
`/org/${orgId}/user/${user.userId}`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const res = await getOrgUser();
|
const res = await getOrgUser();
|
||||||
orgUser = res.data.data;
|
orgUser = res.data.data;
|
||||||
@@ -48,8 +48,8 @@ export default async function GeneralSettingsPage({
|
|||||||
const getOrg = cache(async () =>
|
const getOrg = cache(async () =>
|
||||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||||
`/org/${orgId}`,
|
`/org/${orgId}`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const res = await getOrg();
|
const res = await getOrg();
|
||||||
org = res.data.data;
|
org = res.data.data;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/auth/login");
|
redirect(`/?redirect=/${params.orgId}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookie = await authCookieHeader();
|
const cookie = await authCookieHeader();
|
||||||
@@ -64,8 +64,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
const getOrgUser = cache(() =>
|
const getOrgUser = cache(() =>
|
||||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||||
`/org/${params.orgId}/user/${user.userId}`,
|
`/org/${params.orgId}/user/${user.userId}`,
|
||||||
cookie
|
cookie,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const orgUser = await getOrgUser();
|
const orgUser = await getOrgUser();
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
let orgs: ListOrgsResponse["orgs"] = [];
|
let orgs: ListOrgsResponse["orgs"] = [];
|
||||||
try {
|
try {
|
||||||
const getOrgs = cache(() =>
|
const getOrgs = cache(() =>
|
||||||
internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie)
|
internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie),
|
||||||
);
|
);
|
||||||
const res = await getOrgs();
|
const res = await getOrgs();
|
||||||
if (res && res.data.data.orgs) {
|
if (res && res.data.data.orgs) {
|
||||||
@@ -91,7 +91,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full border-b bg-neutral-100 dark:bg-neutral-900 mb-6 select-none sm:px-0 px-3 pt-3">
|
<div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 mb-6 select-none sm:px-0 px-3 pt-3">
|
||||||
<div className="container mx-auto flex flex-col content-between gap-4 ">
|
<div className="container mx-auto flex flex-col content-between gap-4 ">
|
||||||
<Header
|
<Header
|
||||||
email={user.email}
|
email={user.email}
|
||||||
|
|||||||
@@ -305,11 +305,11 @@ export default function ResourceAuthenticationPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-12">
|
<div className="space-y-12 lg:max-w-2xl">
|
||||||
<section className="space-y-8">
|
<section className="space-y-8">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Users & Roles"
|
title="Users & Roles"
|
||||||
description="Configure which users can access this resource (only applicable if SSO enabled)"
|
description="Configure which users and roles can visit this resource"
|
||||||
size="1xl"
|
size="1xl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -320,11 +320,13 @@ export default function ResourceAuthenticationPage() {
|
|||||||
defaultChecked={resource.sso}
|
defaultChecked={resource.sso}
|
||||||
onCheckedChange={(val) => setSsoEnabled(val)}
|
onCheckedChange={(val) => setSsoEnabled(val)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="sso-toggle">Allow SSO</Label>
|
<Label htmlFor="sso-toggle">
|
||||||
|
Allow Unified Login
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground text-sm">
|
<span className="text-muted-foreground text-sm">
|
||||||
Existing users will only have to login once for all
|
Existing users will only have to login once for all
|
||||||
resources that have SSO enabled.
|
resources that have this enabled.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -460,7 +462,7 @@ export default function ResourceAuthenticationPage() {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<section className="space-y-8 lg:max-w-2xl">
|
<section className="space-y-8">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Authentication Methods"
|
title="Authentication Methods"
|
||||||
description="Allow anyone to access the resource via the below methods"
|
description="Allow anyone to access the resource via the below methods"
|
||||||
|
|||||||
@@ -233,6 +233,24 @@ export default function ReverseProxyTargets(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnDef<LocalTarget>[] = [
|
const columns: ColumnDef<LocalTarget>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "method",
|
||||||
|
header: "Method",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Select
|
||||||
|
defaultValue={row.original.method}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTarget(row.original.targetId, { method: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>{row.original.method}</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">http</SelectItem>
|
||||||
|
<SelectItem value="https">https</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "ip",
|
||||||
header: "IP Address",
|
header: "IP Address",
|
||||||
@@ -262,24 +280,6 @@ export default function ReverseProxyTargets(props: {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: "method",
|
|
||||||
header: "Method",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Select
|
|
||||||
defaultValue={row.original.method}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTarget(row.original.targetId, { method: value })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>{row.original.method}</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="http">http</SelectItem>
|
|
||||||
<SelectItem value="https">https</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// {
|
// {
|
||||||
// accessorKey: "protocol",
|
// accessorKey: "protocol",
|
||||||
// header: "Protocol",
|
// header: "Protocol",
|
||||||
@@ -368,7 +368,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr />
|
<hr className="lg:max-w-2xl" />
|
||||||
|
|
||||||
<section className="space-y-8">
|
<section className="space-y-8">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
@@ -386,25 +386,6 @@ export default function ReverseProxyTargets(props: {
|
|||||||
className="space-y-8"
|
className="space-y-8"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<FormField
|
|
||||||
control={addTargetForm.control}
|
|
||||||
name="ip"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
IP Address
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input id="ip" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Enter the IP address of the
|
|
||||||
target.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={addTargetForm.control}
|
control={addTargetForm.control}
|
||||||
name="method"
|
name="method"
|
||||||
@@ -444,6 +425,25 @@ export default function ReverseProxyTargets(props: {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={addTargetForm.control}
|
||||||
|
name="ip"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
IP Address
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input id="ip" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter the IP address of the
|
||||||
|
target.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={addTargetForm.control}
|
control={addTargetForm.control}
|
||||||
name="port"
|
name="port"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { ListSitesResponse } from "@server/routers/site";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import api from "@app/api";
|
import api from "@app/api";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { GetResourceAuthInfoResponse } from "@server/routers/resource";
|
import { GetResourceAuthInfoResponse } from "@server/routers/resource";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
@@ -57,6 +57,7 @@ export default function GeneralForm() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { resource, updateResource } = useResourceContext();
|
const { resource, updateResource } = useResourceContext();
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const orgId = params.orgId;
|
const orgId = params.orgId;
|
||||||
|
|
||||||
@@ -112,6 +113,8 @@ export default function GeneralForm() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
updateResource({ name: data.name, subdomain: data.subdomain });
|
updateResource({ name: data.name, subdomain: data.subdomain });
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
})
|
})
|
||||||
.finally(() => setSaveLoading(false));
|
.finally(() => setSaveLoading(false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,11 +59,6 @@ const accountFormSchema = z.object({
|
|||||||
|
|
||||||
type AccountFormValues = z.infer<typeof accountFormSchema>;
|
type AccountFormValues = z.infer<typeof accountFormSchema>;
|
||||||
|
|
||||||
const defaultValues: Partial<AccountFormValues> = {
|
|
||||||
subdomain: "",
|
|
||||||
name: "My Resource",
|
|
||||||
};
|
|
||||||
|
|
||||||
type CreateResourceFormProps = {
|
type CreateResourceFormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
@@ -88,7 +83,10 @@ export default function CreateResourceForm({
|
|||||||
|
|
||||||
const form = useForm<AccountFormValues>({
|
const form = useForm<AccountFormValues>({
|
||||||
resolver: zodResolver(accountFormSchema),
|
resolver: zodResolver(accountFormSchema),
|
||||||
defaultValues,
|
defaultValues: {
|
||||||
|
subdomain: "",
|
||||||
|
name: "My Resource",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,9 +96,13 @@ export default function CreateResourceForm({
|
|||||||
|
|
||||||
const fetchSites = async () => {
|
const fetchSites = async () => {
|
||||||
const res = await api.get<AxiosResponse<ListSitesResponse>>(
|
const res = await api.get<AxiosResponse<ListSitesResponse>>(
|
||||||
`/org/${orgId}/sites/`
|
`/org/${orgId}/sites/`,
|
||||||
);
|
);
|
||||||
setSites(res.data.data.sites);
|
setSites(res.data.data.sites);
|
||||||
|
|
||||||
|
if (res.data.data.sites.length > 0) {
|
||||||
|
form.setValue("siteId", res.data.data.sites[0].siteId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchSites();
|
fetchSites();
|
||||||
@@ -116,7 +118,7 @@ export default function CreateResourceForm({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
// subdomain: data.subdomain,
|
// subdomain: data.subdomain,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
@@ -124,7 +126,7 @@ export default function CreateResourceForm({
|
|||||||
title: "Error creating resource",
|
title: "Error creating resource",
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
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) =>
|
onChange={(value) =>
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"subdomain",
|
"subdomain",
|
||||||
value
|
value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -225,14 +227,14 @@ export default function CreateResourceForm({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-[350px] justify-between",
|
"w-[350px] justify-between",
|
||||||
!field.value &&
|
!field.value &&
|
||||||
"text-muted-foreground"
|
"text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field.value
|
{field.value
|
||||||
? sites.find(
|
? sites.find(
|
||||||
(site) =>
|
(site) =>
|
||||||
site.siteId ===
|
site.siteId ===
|
||||||
field.value
|
field.value,
|
||||||
)?.name
|
)?.name
|
||||||
: "Select site"}
|
: "Select site"}
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
@@ -259,7 +261,7 @@ export default function CreateResourceForm({
|
|||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"siteId",
|
"siteId",
|
||||||
site.siteId
|
site.siteId,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -269,14 +271,14 @@ export default function CreateResourceForm({
|
|||||||
site.siteId ===
|
site.siteId ===
|
||||||
field.value
|
field.value
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0"
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
site.name
|
site.name
|
||||||
}
|
}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { Button } from "@app/components/ui/button";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus, Search } from "lucide-react";
|
||||||
|
|
||||||
interface ResourcesDataTableProps<TData, TValue> {
|
interface ResourcesDataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
@@ -62,19 +62,23 @@ export function ResourcesDataTable<TData, TValue>({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between pb-4">
|
<div className="flex items-center justify-between pb-4">
|
||||||
<Input
|
<div className="flex items-center max-w-sm mr-2 w-full relative">
|
||||||
placeholder="Search resources"
|
<Input
|
||||||
value={
|
placeholder="Search resources"
|
||||||
(table.getColumn("name")?.getFilterValue() as string) ??
|
value={
|
||||||
""
|
(table
|
||||||
}
|
.getColumn("name")
|
||||||
onChange={(event) =>
|
?.getFilterValue() as string) ?? ""
|
||||||
table
|
}
|
||||||
.getColumn("name")
|
onChange={(event) =>
|
||||||
?.setFilterValue(event.target.value)
|
table
|
||||||
}
|
.getColumn("name")
|
||||||
className="max-w-sm mr-2"
|
?.setFilterValue(event.target.value)
|
||||||
/>
|
}
|
||||||
|
className="w-full pl-8"
|
||||||
|
/>
|
||||||
|
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (addResource) {
|
if (addResource) {
|
||||||
@@ -85,7 +89,7 @@ export function ResourcesDataTable<TData, TValue>({
|
|||||||
<Plus className="mr-2 h-4 w-4" /> Add Resource
|
<Plus className="mr-2 h-4 w-4" /> Add Resource
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="border rounded-md">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -98,7 +102,7 @@ export function ResourcesDataTable<TData, TValue>({
|
|||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef
|
header.column.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext()
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
@@ -119,7 +123,7 @@ export function ResourcesDataTable<TData, TValue>({
|
|||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext()
|
cell.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,7 +9,16 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
import {
|
||||||
|
Copy,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowUpDown,
|
||||||
|
MoreHorizontal,
|
||||||
|
Check,
|
||||||
|
ArrowUpRight,
|
||||||
|
ShieldOff,
|
||||||
|
ShieldCheck,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import api from "@app/api";
|
import api from "@app/api";
|
||||||
@@ -26,6 +35,8 @@ export type ResourceRow = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
site: string;
|
site: string;
|
||||||
|
siteId: string;
|
||||||
|
hasAuth: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResourcesTableProps = {
|
type ResourcesTableProps = {
|
||||||
@@ -91,10 +102,111 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return (
|
||||||
|
<Button variant="outline">
|
||||||
|
<Link
|
||||||
|
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.siteId}`}
|
||||||
|
>
|
||||||
|
{resourceRow.site}
|
||||||
|
</Link>
|
||||||
|
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "domain",
|
accessorKey: "domain",
|
||||||
header: "Domain",
|
header: "Full URL",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link
|
||||||
|
href={resourceRow.domain}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:underline mr-2"
|
||||||
|
>
|
||||||
|
{resourceRow.domain}
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
resourceRow.domain,
|
||||||
|
);
|
||||||
|
const originalIcon = document.querySelector(
|
||||||
|
`#icon-${resourceRow.id}`,
|
||||||
|
);
|
||||||
|
if (originalIcon) {
|
||||||
|
originalIcon.classList.add("hidden");
|
||||||
|
}
|
||||||
|
const checkIcon = document.querySelector(
|
||||||
|
`#check-icon-${resourceRow.id}`,
|
||||||
|
);
|
||||||
|
if (checkIcon) {
|
||||||
|
checkIcon.classList.remove("hidden");
|
||||||
|
setTimeout(() => {
|
||||||
|
checkIcon.classList.add("hidden");
|
||||||
|
if (originalIcon) {
|
||||||
|
originalIcon.classList.remove(
|
||||||
|
"hidden",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy
|
||||||
|
id={`icon-${resourceRow.id}`}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Check
|
||||||
|
id={`check-icon-${resourceRow.id}`}
|
||||||
|
className="hidden text-green-500 h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Copy domain</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "hasAuth",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Authentication
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{resourceRow.hasAuth ? (
|
||||||
|
<span className="text-green-500 flex items-center space-x-2">
|
||||||
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
<span>Protected</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-yellow-500 flex items-center space-x-2">
|
||||||
|
<ShieldOff className="w-4 h-4" />
|
||||||
|
<span>Not Protected</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
@@ -130,28 +242,25 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedResource(
|
setSelectedResource(
|
||||||
resourceRow
|
resourceRow,
|
||||||
);
|
);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
className="text-red-600 hover:text-red-800 hover:underline cursor-pointer"
|
className="text-red-500"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Button
|
<Link
|
||||||
variant={"gray"}
|
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||||
className="ml-2"
|
|
||||||
onClick={() =>
|
|
||||||
router.push(
|
|
||||||
`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Edit <ArrowRight className="ml-2 w-4 h-4" />
|
<Button variant={"gray"} className="ml-2">
|
||||||
</Button>
|
Edit
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
|||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
|
const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
|
||||||
`/org/${params.orgId}/resources`,
|
`/org/${params.orgId}/resources`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
);
|
);
|
||||||
resources = res.data.data.resources;
|
resources = res.data.data.resources;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -31,8 +31,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
|||||||
const getOrg = cache(async () =>
|
const getOrg = cache(async () =>
|
||||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||||
`/org/${params.orgId}`,
|
`/org/${params.orgId}`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const res = await getOrg();
|
const res = await getOrg();
|
||||||
org = res.data.data;
|
org = res.data.data;
|
||||||
@@ -49,8 +49,13 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
|||||||
id: resource.resourceId,
|
id: resource.resourceId,
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
domain: resource.subdomain || "",
|
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
|
||||||
site: resource.siteName || "None",
|
site: resource.siteName || "None",
|
||||||
|
siteId: resource.siteId || "Unknown",
|
||||||
|
hasAuth:
|
||||||
|
resource.sso ||
|
||||||
|
resource.pincodeId !== null ||
|
||||||
|
resource.pincodeId !== null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { Button } from "@app/components/ui/button";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { DataTablePagination } from "../../../../../components/DataTablePagination";
|
import { DataTablePagination } from "../../../../../components/DataTablePagination";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus, Search } from "lucide-react";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
@@ -62,19 +62,23 @@ export function SitesDataTable<TData, TValue>({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between pb-4">
|
<div className="flex items-center justify-between pb-4">
|
||||||
<Input
|
<div className="flex items-center max-w-sm mr-2 w-full relative">
|
||||||
placeholder="Search sites"
|
<Input
|
||||||
value={
|
placeholder="Search sites"
|
||||||
(table.getColumn("name")?.getFilterValue() as string) ??
|
value={
|
||||||
""
|
(table
|
||||||
}
|
.getColumn("name")
|
||||||
onChange={(event) =>
|
?.getFilterValue() as string) ?? ""
|
||||||
table
|
}
|
||||||
.getColumn("name")
|
onChange={(event) =>
|
||||||
?.setFilterValue(event.target.value)
|
table
|
||||||
}
|
.getColumn("name")
|
||||||
className="max-w-sm mr-2"
|
?.setFilterValue(event.target.value)
|
||||||
/>
|
}
|
||||||
|
className="w-full pl-8"
|
||||||
|
/>
|
||||||
|
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (addSite) {
|
if (addSite) {
|
||||||
@@ -85,7 +89,7 @@ export function SitesDataTable<TData, TValue>({
|
|||||||
<Plus className="mr-2 h-4 w-4" /> Add Site
|
<Plus className="mr-2 h-4 w-4" /> Add Site
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="border rounded-md">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -98,7 +102,7 @@ export function SitesDataTable<TData, TValue>({
|
|||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef
|
header.column.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext()
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
@@ -119,7 +123,7 @@ export function SitesDataTable<TData, TValue>({
|
|||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext()
|
cell.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ export type SiteRow = {
|
|||||||
id: number;
|
id: number;
|
||||||
nice: string;
|
nice: string;
|
||||||
name: string;
|
name: string;
|
||||||
mbIn: number;
|
mbIn: string;
|
||||||
mbOut: number;
|
mbOut: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
type: "newt" | "wireguard";
|
||||||
};
|
};
|
||||||
|
|
||||||
type SitesTableProps = {
|
type SitesTableProps = {
|
||||||
@@ -99,11 +100,70 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "mbIn",
|
accessorKey: "mbIn",
|
||||||
header: "MB In",
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Data In
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "mbOut",
|
accessorKey: "mbOut",
|
||||||
header: "MB Out",
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Data Out
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "type",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Connection Type
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const originalRow = row.original;
|
||||||
|
|
||||||
|
if (originalRow.type === "newt") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>Newt</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalRow.type === "wireguard") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>WireGuard</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
@@ -135,24 +195,21 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||||||
setSelectedSite(siteRow);
|
setSelectedSite(siteRow);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
className="text-red-600 hover:text-red-800"
|
className="text-red-500"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Button
|
<Link
|
||||||
variant={"gray"}
|
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||||
className="ml-2"
|
|
||||||
onClick={() =>
|
|
||||||
router.push(
|
|
||||||
`/${siteRow.orgId}/settings/sites/${siteRow.nice}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Edit <ArrowRight className="ml-2 w-4 h-4" />
|
<Button variant={"gray"} className="ml-2">
|
||||||
</Button>
|
Edit
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,21 +15,32 @@ export default async function SitesPage(props: SitesPageProps) {
|
|||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<ListSitesResponse>>(
|
const res = await internal.get<AxiosResponse<ListSitesResponse>>(
|
||||||
`/org/${params.orgId}/sites`,
|
`/org/${params.orgId}/sites`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
);
|
);
|
||||||
sites = res.data.data.sites;
|
sites = res.data.data.sites;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error fetching sites", 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) => {
|
const siteRows: SiteRow[] = sites.map((site) => {
|
||||||
return {
|
return {
|
||||||
name: site.name,
|
name: site.name,
|
||||||
id: site.siteId,
|
id: site.siteId,
|
||||||
nice: site.niceId.toString(),
|
nice: site.niceId.toString(),
|
||||||
mbIn: site.megabytesIn || 0,
|
mbIn: formatSize(site.megabytesIn || 0),
|
||||||
mbOut: site.megabytesOut || 0,
|
mbOut: formatSize(site.megabytesOut || 0),
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
|
type: site.type as any,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,7 +48,7 @@ export default async function SitesPage(props: SitesPageProps) {
|
|||||||
<>
|
<>
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Manage Sites"
|
title="Manage Sites"
|
||||||
description="Manage your existing sites here or create a new one."
|
description="Allow connectivity to your network through secure tunnels"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SitesTable sites={siteRows} orgId={params.orgId} />
|
<SitesTable sites={siteRows} orgId={params.orgId} />
|
||||||
|
|||||||
@@ -30,7 +30,15 @@ export default function DashboardLoginForm({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
redirect={redirect}
|
redirect={redirect}
|
||||||
onLogin={() => router.push("/")}
|
onLogin={() => {
|
||||||
|
if (redirect && redirect.includes("http")) {
|
||||||
|
window.location.href = redirect;
|
||||||
|
} else if (redirect) {
|
||||||
|
router.push(redirect);
|
||||||
|
} else {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -5,8 +8,9 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@app/components/ui/card";
|
} from "@app/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function ResourceAccessDenied() {
|
export default function ResourceAccessDenied() {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -17,6 +21,11 @@ export default async function ResourceAccessDenied() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
You're not alowed to access this resource. If this is a mistake,
|
You're not alowed to access this resource. If this is a mistake,
|
||||||
please contact the administrator.
|
please contact the administrator.
|
||||||
|
<div className="text-center mt-4">
|
||||||
|
<Button>
|
||||||
|
<Link href="/">Go Home</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { LockIcon, UserIcon, Binary, Key, User } from "lucide-react";
|
import { LockIcon, Binary, Key, User } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
@@ -34,10 +34,10 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
import { formatAxiosError } from "@app/lib/utils";
|
import { formatAxiosError } from "@app/lib/utils";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { LoginResponse } from "@server/routers/auth";
|
|
||||||
import ResourceAccessDenied from "./ResourceAccessDenied";
|
|
||||||
import LoginForm from "@app/components/LoginForm";
|
import LoginForm from "@app/components/LoginForm";
|
||||||
import { AuthWithPasswordResponse } from "@server/routers/resource";
|
import { AuthWithPasswordResponse } from "@server/routers/resource";
|
||||||
|
import { redirect } from "next/dist/server/api-utils";
|
||||||
|
import ResourceAccessDenied from "./ResourceAccessDenied";
|
||||||
|
|
||||||
const pinSchema = z.object({
|
const pinSchema = z.object({
|
||||||
pin: z
|
pin: z
|
||||||
@@ -63,7 +63,6 @@ type ResourceAuthPortalProps = {
|
|||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
redirect: string;
|
redirect: string;
|
||||||
queryParamName: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
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<typeof pinSchema>) => {
|
const onPinSubmit = (values: z.infer<typeof pinSchema>) => {
|
||||||
setLoadingLogin(true);
|
setLoadingLogin(true);
|
||||||
api.post<AxiosResponse<AuthWithPasswordResponse>>(
|
api.post<AxiosResponse<AuthWithPasswordResponse>>(
|
||||||
@@ -130,10 +122,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
const session = res.data.data.session;
|
const session = res.data.data.session;
|
||||||
if (session) {
|
if (session) {
|
||||||
window.location.href = constructRedirect(
|
window.location.href = props.redirect;
|
||||||
props.redirect,
|
|
||||||
session,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -156,10 +145,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
const session = res.data.data.session;
|
const session = res.data.data.session;
|
||||||
if (session) {
|
if (session) {
|
||||||
window.location.href = constructRedirect(
|
window.location.href = props.redirect;
|
||||||
props.redirect,
|
|
||||||
session,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -172,13 +158,15 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function handleSSOAuth() {
|
async function handleSSOAuth() {
|
||||||
console.log("SSO authentication");
|
let isAllowed = false;
|
||||||
|
try {
|
||||||
await api.get(`/resource/${props.resource.id}`).catch((e) => {
|
await api.get(`/resource/${props.resource.id}`);
|
||||||
|
isAllowed = true;
|
||||||
|
} catch (e) {
|
||||||
setAccessDenied(true);
|
setAccessDenied(true);
|
||||||
});
|
}
|
||||||
|
|
||||||
if (!accessDenied) {
|
if (isAllowed) {
|
||||||
window.location.href = props.redirect;
|
window.location.href = props.redirect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +175,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
<div>
|
<div>
|
||||||
{!accessDenied ? (
|
{!accessDenied ? (
|
||||||
<div>
|
<div>
|
||||||
|
<div className="text-center mb-2">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Powered by Fossorial
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Authentication Required</CardTitle>
|
<CardTitle>Authentication Required</CardTitle>
|
||||||
@@ -378,6 +371,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
className={`${numMethods <= 1 ? "mt-0" : ""}`}
|
className={`${numMethods <= 1 ? "mt-0" : ""}`}
|
||||||
>
|
>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
|
redirect={
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? window.location.href
|
||||||
|
: ""
|
||||||
|
}
|
||||||
onLogin={async () =>
|
onLogin={async () =>
|
||||||
await handleSSOAuth()
|
await handleSSOAuth()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -5,6 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@app/components/ui/card";
|
} from "@app/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function ResourceNotFound() {
|
export default async function ResourceNotFound() {
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +17,12 @@ export default async function ResourceNotFound() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
The resource you're trying to access does not exist
|
The resource you're trying to access does not exist.
|
||||||
|
<div className="text-center mt-4">
|
||||||
|
<Button>
|
||||||
|
<Link href="/">Go Home</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
GetResourceResponse,
|
GetResourceResponse,
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
import ResourceAuthPortal from "./components/ResourceAuthPortal";
|
import ResourceAuthPortal from "./components/ResourceAuthPortal";
|
||||||
import { internal } from "@app/api";
|
import { internal, priv } from "@app/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { authCookieHeader } from "@app/api/cookies";
|
import { authCookieHeader } from "@app/api/cookies";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
@@ -11,10 +11,12 @@ import { verifySession } from "@app/lib/auth/verifySession";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import ResourceNotFound from "./components/ResourceNotFound";
|
import ResourceNotFound from "./components/ResourceNotFound";
|
||||||
import ResourceAccessDenied from "./components/ResourceAccessDenied";
|
import ResourceAccessDenied from "./components/ResourceAccessDenied";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { CheckResourceSessionResponse } from "@server/routers/auth";
|
||||||
|
|
||||||
export default async function ResourceAuthPage(props: {
|
export default async function ResourceAuthPage(props: {
|
||||||
params: Promise<{ resourceId: number }>;
|
params: Promise<{ resourceId: number }>;
|
||||||
searchParams: Promise<{ redirect: string }>;
|
searchParams: Promise<{ redirect: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
@@ -31,7 +33,7 @@ export default async function ResourceAuthPage(props: {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
const getUser = cache(verifySession);
|
const getUser = cache(verifySession);
|
||||||
const user = await getUser();
|
const user = await getUser({ skipCheckVerifyEmail: true });
|
||||||
|
|
||||||
if (!authInfo) {
|
if (!authInfo) {
|
||||||
return (
|
return (
|
||||||
@@ -46,6 +48,40 @@ export default async function ResourceAuthPage(props: {
|
|||||||
|
|
||||||
const redirectUrl = searchParams.redirect || authInfo.url;
|
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<CheckResourceSessionResponse>
|
||||||
|
>(`/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) {
|
if (!hasAuth) {
|
||||||
// no authentication so always go straight to the resource
|
// no authentication so always go straight to the resource
|
||||||
redirect(redirectUrl);
|
redirect(redirectUrl);
|
||||||
@@ -63,7 +99,6 @@ export default async function ResourceAuthPage(props: {
|
|||||||
console.log(res.data);
|
console.log(res.data);
|
||||||
doRedirect = true;
|
doRedirect = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
|
||||||
userIsUnauthorized = true;
|
userIsUnauthorized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,33 +107,28 @@ export default async function ResourceAuthPage(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userIsUnauthorized && isSSOOnly) {
|
|
||||||
return (
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<ResourceAccessDenied />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full max-w-md">
|
{userIsUnauthorized && isSSOOnly ? (
|
||||||
<ResourceAuthPortal
|
<div className="w-full max-w-md">
|
||||||
methods={{
|
<ResourceAccessDenied />
|
||||||
password: authInfo.password,
|
</div>
|
||||||
pincode: authInfo.pincode,
|
) : (
|
||||||
sso: authInfo.sso && !userIsUnauthorized,
|
<div className="w-full max-w-md">
|
||||||
}}
|
<ResourceAuthPortal
|
||||||
resource={{
|
methods={{
|
||||||
name: authInfo.resourceName,
|
password: authInfo.password,
|
||||||
id: authInfo.resourceId,
|
pincode: authInfo.pincode,
|
||||||
}}
|
sso: authInfo.sso && !userIsUnauthorized,
|
||||||
redirect={redirectUrl}
|
}}
|
||||||
queryParamName={
|
resource={{
|
||||||
process.env.RESOURCE_SESSION_QUERY_PARAM_NAME!
|
name: authInfo.resourceName,
|
||||||
}
|
id: authInfo.resourceId,
|
||||||
/>
|
}}
|
||||||
</div>
|
redirect={redirectUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export default function VerifyEmailForm({
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setError(formatAxiosError(e, "An error occurred"));
|
setError(formatAxiosError(e, "An error occurred"));
|
||||||
console.error("Failed to verify email:", e);
|
console.error("Failed to verify email:", e);
|
||||||
|
setIsSubmitting(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.data?.data?.valid) {
|
if (res && res.data?.data?.valid) {
|
||||||
@@ -125,7 +126,7 @@ export default function VerifyEmailForm({
|
|||||||
<div>
|
<div>
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Verify Your Email</CardTitle>
|
<CardTitle>Verify Email</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Enter the verification code sent to your email address.
|
Enter the verification code sent to your email address.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -234,7 +235,7 @@ export default function VerifyEmailForm({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="text-center text-muted-foreground mt-4">
|
<div className="text-center text-muted-foreground mt-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="link"
|
variant="link"
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ export const dynamic = "force-dynamic";
|
|||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
if (process.env.PUBLIC_FLAGS_EMAIL_VERIFICATION_REQUIRED !== "true") {
|
if (process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED !== "true") {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const getUser = cache(verifySession);
|
const getUser = cache(verifySession);
|
||||||
const user = await getUser();
|
const user = await getUser({ skipCheckVerifyEmail: true });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 20 14.3% 4.1%;
|
--foreground: 20 5.0% 10.0%;
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 20 14.3% 4.1%;
|
--card-foreground: 20 5.0% 10.0%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 20 14.3% 4.1%;
|
--popover-foreground: 20 5.0% 10.0%;
|
||||||
--primary: 24.6 95% 53.1%;
|
--primary: 24.6 95% 53.1%;
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
--secondary: 60 4.8% 95.9%;
|
--secondary: 60 4.8% 95.9%;
|
||||||
@@ -33,24 +33,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 20 14.3% 4.1%;
|
--background: 20 5.0% 10.0%;
|
||||||
--foreground: 60 9.1% 97.8%;
|
--foreground: 60 9.1% 97.8%;
|
||||||
--card: 20 14.3% 4.1%;
|
--card: 20 5.0% 10.0%;
|
||||||
--card-foreground: 60 9.1% 97.8%;
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
--popover: 20 14.3% 4.1%;
|
--popover: 20 5.0% 10.0%;
|
||||||
--popover-foreground: 60 9.1% 97.8%;
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
--primary: 20.5 90.2% 48.2%;
|
--primary: 20.5 90.2% 48.2%;
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
--secondary: 12 6.5% 15.1%;
|
--secondary: 12 6.5% 25.0%;
|
||||||
--secondary-foreground: 60 9.1% 97.8%;
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
--muted: 12 6.5% 15.1%;
|
--muted: 12 6.5% 25.0%;
|
||||||
--muted-foreground: 24 5.4% 63.9%;
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
--accent: 12 6.5% 15.1%;
|
--accent: 12 6.5% 25.0%;
|
||||||
--accent-foreground: 60 9.1% 97.8%;
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 72.2% 50.6%;
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
--border: 12 6.5% 15.1%;
|
--border: 12 6.5% 25.0%;
|
||||||
--input: 12 6.5% 15.1%;
|
--input: 12 6.5% 25.0%;
|
||||||
--ring: 20.5 90.2% 48.2%;
|
--ring: 20.5 90.2% 48.2%;
|
||||||
--chart-1: 220 70% 50%;
|
--chart-1: 220 70% 50%;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-2: 160 60% 45%;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default async function InvitePage(props: {
|
|||||||
const user = await verifySession();
|
const user = await verifySession();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(`/auth/login?redirect=/invite?token=${params.token}`);
|
redirect(`/?redirect=/invite?token=${params.token}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = tokenParam.split("-");
|
const parts = tokenParam.split("-");
|
||||||
|
|||||||
@@ -12,21 +12,36 @@ import { cache } from "react";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
searchParams: Promise<{ redirect: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const params = await props.searchParams; // this is needed to prevent static optimization
|
const params = await props.searchParams; // this is needed to prevent static optimization
|
||||||
const getUser = cache(verifySession);
|
const getUser = cache(verifySession);
|
||||||
const user = await getUser();
|
const user = await getUser({ skipCheckVerifyEmail: true });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/auth/login");
|
if (params.redirect) {
|
||||||
|
redirect(`/auth/login?redirect=${params.redirect}`);
|
||||||
|
} else {
|
||||||
|
redirect(`/auth/login`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!user.emailVerified &&
|
||||||
|
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true"
|
||||||
|
) {
|
||||||
|
if (params.redirect) {
|
||||||
|
redirect(`/auth/verify-email?redirect=${params.redirect}`);
|
||||||
|
} else {
|
||||||
|
redirect(`/auth/verify-email`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let orgs: ListOrgsResponse["orgs"] = [];
|
let orgs: ListOrgsResponse["orgs"] = [];
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
|
const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
|
||||||
`/orgs`,
|
`/orgs`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res && res.data.data.orgs) {
|
if (res && res.data.data.orgs) {
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { cache } from "react";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: `Setup - Pangolin`,
|
title: `Setup - Pangolin`,
|
||||||
description: "",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function SetupLayout({
|
export default async function SetupLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const getUser = cache(verifySession);
|
||||||
|
const user = await getUser();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
redirect("/?redirect=/setup");
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mt-32">{children}</div>;
|
return <div className="mt-32">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function StepperForm() {
|
|||||||
|
|
||||||
const debouncedCheckOrgIdAvailability = useCallback(
|
const debouncedCheckOrgIdAvailability = useCallback(
|
||||||
debounce(checkOrgIdAvailability, 300),
|
debounce(checkOrgIdAvailability, 300),
|
||||||
[checkOrgIdAvailability]
|
[checkOrgIdAvailability],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -278,7 +278,7 @@ export default function StepperForm() {
|
|||||||
|
|
||||||
function debounce<T extends (...args: any[]) => any>(
|
function debounce<T extends (...args: any[]) => any>(
|
||||||
func: T,
|
func: T,
|
||||||
wait: number
|
wait: number,
|
||||||
): (...args: Parameters<T>) => void {
|
): (...args: Parameters<T>) => void {
|
||||||
let timeout: NodeJS.Timeout | null = null;
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
|
|||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
|
|
||||||
if (res.data?.data?.emailVerificationRequired) {
|
if (res.data?.data?.emailVerificationRequired) {
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
router.push(`/auth/verify-email?redirect=${redirect}`);
|
router.push(`/auth/verify-email?redirect=${redirect}`);
|
||||||
@@ -86,14 +84,8 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirect && redirect.includes("http")) {
|
if (onLogin) {
|
||||||
window.location.href = redirect;
|
await onLogin();
|
||||||
} else if (redirect) {
|
|
||||||
router.push(redirect);
|
|
||||||
} else {
|
|
||||||
if (onLogin) {
|
|
||||||
await onLogin();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,33 @@ import { authCookieHeader } from "@app/api/cookies";
|
|||||||
import { GetUserResponse } from "@server/routers/user";
|
import { GetUserResponse } from "@server/routers/user";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
|
|
||||||
export async function verifySession(): Promise<GetUserResponse | null> {
|
export async function verifySession({
|
||||||
|
skipCheckVerifyEmail,
|
||||||
|
}: {
|
||||||
|
skipCheckVerifyEmail?: boolean;
|
||||||
|
} = {}): Promise<GetUserResponse | null> {
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<GetUserResponse>>(
|
const res = await internal.get<AxiosResponse<GetUserResponse>>(
|
||||||
"/user",
|
"/user",
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.data.data;
|
const user = res.data.data;
|
||||||
} catch {
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!skipCheckVerifyEmail &&
|
||||||
|
!user.emailVerified &&
|
||||||
|
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED == "true"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user