mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-12 21:30:35 +01:00
Merge branch 'dev' into feature/newt-podman-install
This commit is contained in:
commit
cf80d67bf8
62 changed files with 2206 additions and 1898 deletions
|
@ -10,7 +10,7 @@ services:
|
|||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "3s"
|
||||
timeout: "3s"
|
||||
retries: 5
|
||||
retries: 15
|
||||
|
||||
gerbil:
|
||||
image: fosrl/gerbil:latest
|
||||
|
|
|
@ -10,7 +10,7 @@ services:
|
|||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "3s"
|
||||
timeout: "3s"
|
||||
retries: 5
|
||||
retries: 15
|
||||
{{if .InstallGerbil}}
|
||||
gerbil:
|
||||
image: fosrl/gerbil:{{.GerbilVersion}}
|
||||
|
|
2824
package-lock.json
generated
2824
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
@ -20,6 +20,7 @@
|
|||
"email": "email dev --dir server/emails/templates --port 3005"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.0",
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@node-rs/argon2": "2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
|
@ -39,11 +40,13 @@
|
|||
"@radix-ui/react-switch": "1.1.2",
|
||||
"@radix-ui/react-tabs": "1.1.2",
|
||||
"@radix-ui/react-toast": "1.2.4",
|
||||
"@react-email/components": "0.0.31",
|
||||
"@react-email/components": "0.0.36",
|
||||
"@react-email/render": "^1.0.6",
|
||||
"@react-email/tailwind": "1.0.4",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"axios": "1.7.9",
|
||||
"axios": "1.8.4",
|
||||
"better-sqlite3": "11.7.0",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
|
@ -62,7 +65,7 @@
|
|||
"js-yaml": "4.1.0",
|
||||
"lucide-react": "0.469.0",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.1.3",
|
||||
"next": "15.2.4",
|
||||
"next-themes": "0.4.4",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
|
@ -77,6 +80,7 @@
|
|||
"react-icons": "^5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"semver": "7.6.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"vaul": "1.1.2",
|
||||
|
@ -99,16 +103,17 @@
|
|||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/ws": "8.5.13",
|
||||
"@types/yargs": "17.0.33",
|
||||
"drizzle-kit": "0.30.1",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild-node-externals": "1.16.0",
|
||||
"drizzle-kit": "0.30.6",
|
||||
"esbuild": "0.25.2",
|
||||
"esbuild-node-externals": "1.18.0",
|
||||
"postcss": "^8",
|
||||
"react-email": "3.0.4",
|
||||
"react-email": "4.0.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsx": "4.19.2",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
|
|
6
server/extendZod.ts
Normal file
6
server/extendZod.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
|
||||
import { z } from "zod";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export default function extendZod() {}
|
|
@ -1,3 +1,5 @@
|
|||
import "./extendZod.ts";
|
||||
|
||||
import { runSetupFunctions } from "./setup";
|
||||
import { createApiServer } from "./apiServer";
|
||||
import { createNextServer } from "./nextServer";
|
||||
|
|
14
server/openApi.ts
Normal file
14
server/openApi.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
|
||||
|
||||
export const registry = new OpenAPIRegistry();
|
||||
|
||||
export enum OpenAPITags {
|
||||
Site = "Site",
|
||||
Org = "Organization",
|
||||
Resource = "Resource",
|
||||
Role = "Role",
|
||||
User = "User",
|
||||
Target = "Target",
|
||||
Rule = "Rule",
|
||||
AccessToken = "Access Token"
|
||||
}
|
|
@ -8,6 +8,7 @@ import { fromError } from "zod-validation-error";
|
|||
import { resourceAccessToken } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import db from "@server/db";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteAccessTokenParamsSchema = z
|
||||
.object({
|
||||
|
@ -15,6 +16,17 @@ const deleteAccessTokenParamsSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/access-token/{accessTokenId}",
|
||||
description: "Delete a access token.",
|
||||
tags: [OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: deleteAccessTokenParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteAccessToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -22,6 +22,7 @@ import { createDate, TimeSpan } from "oslo";
|
|||
import { hashPassword } from "@server/auth/password";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
export const generateAccessTokenBodySchema = z
|
||||
.object({
|
||||
|
@ -45,6 +46,24 @@ export type GenerateAccessTokenResponse = Omit<
|
|||
"tokenHash"
|
||||
> & { accessToken: string };
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/access-token",
|
||||
description: "Generate a new access token for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: generateAccssTokenParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: generateAccessTokenBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function generateAccessToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm";
|
|||
import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listAccessTokensParamsSchema = z
|
||||
.object({
|
||||
|
@ -73,10 +74,7 @@ function queryAccessTokens(
|
|||
resources,
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
sites,
|
||||
eq(resources.resourceId, sites.siteId)
|
||||
)
|
||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
|
@ -98,10 +96,7 @@ function queryAccessTokens(
|
|||
resources,
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
sites,
|
||||
eq(resources.resourceId, sites.siteId)
|
||||
)
|
||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
|
@ -123,6 +118,34 @@ export type ListAccessTokensResponse = {
|
|||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/access-tokens",
|
||||
description: "List all access tokens in an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
}),
|
||||
query: listAccessTokensSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/access-tokens",
|
||||
description: "List all access tokens in an organization.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourceId: z.number()
|
||||
}),
|
||||
query: listAccessTokensSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listAccessTokens(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import { eq, sql } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listDomainsParamsSchema = z
|
||||
.object({
|
||||
|
@ -51,6 +52,20 @@ export type ListDomainsResponse = {
|
|||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/domains",
|
||||
description: "List all domains for a organization.",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
}),
|
||||
query: listDomainsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listDomains(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -19,6 +19,7 @@ import { createAdminRole } from "@server/setup/ensureActions";
|
|||
import config from "@server/lib/config";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { defaultRoleAllowedActions } from "../role";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const createOrgSchema = z
|
||||
.object({
|
||||
|
@ -29,6 +30,23 @@ const createOrgSchema = z
|
|||
|
||||
// const MAX_ORGS = 5;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org",
|
||||
description: "Create a new organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createOrgSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createOrg(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -17,6 +17,7 @@ import logger from "@server/logger";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { sendToClient } from "../ws";
|
||||
import { deletePeer } from "../gerbil/peers";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteOrgSchema = z
|
||||
.object({
|
||||
|
@ -26,6 +27,17 @@ const deleteOrgSchema = z
|
|||
|
||||
export type DeleteOrgResponse = {};
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/org/{orgId}",
|
||||
description: "Delete an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
params: deleteOrgSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteOrg(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const getOrgSchema = z
|
||||
.object({
|
||||
|
@ -19,6 +20,17 @@ export type GetOrgResponse = {
|
|||
org: Org;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}",
|
||||
description: "Get an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
params: getOrgSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getOrg(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import { sql, inArray } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listOrgsSchema = z.object({
|
||||
limit: z
|
||||
|
@ -21,7 +22,18 @@ const listOrgsSchema = z.object({
|
|||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative()),
|
||||
.pipe(z.number().int().nonnegative())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/orgs",
|
||||
description: "List all organizations in the system",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
query: listOrgsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export type ListOrgsResponse = {
|
||||
|
@ -57,13 +69,13 @@ export async function listOrgs(
|
|||
pagination: {
|
||||
total: 0,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "No organizations found for the user",
|
||||
status: HttpCode.OK,
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -86,13 +98,13 @@ export async function listOrgs(
|
|||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Organizations retrieved successfully",
|
||||
status: HttpCode.OK,
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const updateOrgParamsSchema = z
|
||||
.object({
|
||||
|
@ -17,7 +18,7 @@ const updateOrgParamsSchema = z
|
|||
|
||||
const updateOrgBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
name: z.string().min(1).max(255).optional()
|
||||
// domain: z.string().min(1).max(255).optional(),
|
||||
})
|
||||
.strict()
|
||||
|
@ -25,6 +26,24 @@ const updateOrgBodySchema = z
|
|||
message: "At least one field must be provided for update"
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}",
|
||||
description: "Update an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
params: updateOrgParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateOrgBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateOrg(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -20,6 +20,7 @@ import { fromError } from "zod-validation-error";
|
|||
import logger from "@server/logger";
|
||||
import { subdomainSchema } from "@server/lib/schemas";
|
||||
import config from "@server/lib/config";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const createResourceParamsSchema = z
|
||||
.object({
|
||||
|
@ -90,6 +91,26 @@ const createRawResourceSchema = z
|
|||
|
||||
export type CreateResourceResponse = Resource;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/site/{siteId}/resource",
|
||||
description: "Create a resource.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
||||
request: {
|
||||
params: createResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createHttpResourceSchema.or(
|
||||
createRawResourceSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
isValidIP,
|
||||
isValidUrlGlobPattern
|
||||
} from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const createResourceRuleSchema = z
|
||||
.object({
|
||||
|
@ -33,6 +34,24 @@ const createResourceRuleParamsSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource/{resourceId}/rule",
|
||||
description: "Create a resource rule.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: createResourceRuleParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createResourceRuleSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createResourceRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { fromError } from "zod-validation-error";
|
|||
import { addPeer } from "../gerbil/peers";
|
||||
import { removeTargets } from "../newt/targets";
|
||||
import { getAllowedIps } from "../target/helpers";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const deleteResourceSchema = z
|
||||
|
@ -22,6 +23,17 @@ const deleteResourceSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}",
|
||||
description: "Delete a resource.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: deleteResourceSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
@ -88,7 +100,11 @@ export async function deleteResource(
|
|||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
removeTargets(newt.newtId, targetsToBeRemoved, deletedResource.protocol);
|
||||
removeTargets(
|
||||
newt.newtId,
|
||||
targetsToBeRemoved,
|
||||
deletedResource.protocol
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,11 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteResourceRuleSchema = z
|
||||
.object({
|
||||
ruleId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
ruleId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
|
@ -22,6 +20,17 @@ const deleteResourceRuleSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}/rule/{ruleId}",
|
||||
description: "Delete a resource rule.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: deleteResourceRuleSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteResourceRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const getResourceSchema = z
|
||||
.object({
|
||||
|
@ -22,6 +23,17 @@ export type GetResourceResponse = Resource & {
|
|||
siteName: string;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}",
|
||||
description: "Get a resource.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: getResourceSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const getResourceWhitelistSchema = z
|
||||
.object({
|
||||
|
@ -31,6 +32,17 @@ export type GetResourceWhitelistResponse = {
|
|||
whitelist: NonNullable<Awaited<ReturnType<typeof queryWhitelist>>>;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/whitelist",
|
||||
description: "Get the whitelist of emails for a specific resource.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: getResourceWhitelistSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getResourceWhitelist(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listResourceRolesSchema = z
|
||||
.object({
|
||||
|
@ -35,6 +36,17 @@ export type ListResourceRolesResponse = {
|
|||
roles: NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/roles",
|
||||
description: "List all roles for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: listResourceRolesSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listResourceRoles(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listResourceRulesParamsSchema = z
|
||||
.object({
|
||||
|
@ -56,6 +57,18 @@ export type ListResourceRulesResponse = {
|
|||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/rules",
|
||||
description: "List rules for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: listResourceRulesParamsSchema,
|
||||
query: listResourceRulesSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listResourceRules(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listResourceUsersSchema = z
|
||||
.object({
|
||||
|
@ -33,6 +34,17 @@ export type ListResourceUsersResponse = {
|
|||
users: NonNullable<Awaited<ReturnType<typeof queryUsers>>>;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/users",
|
||||
description: "List all users for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.User],
|
||||
request: {
|
||||
params: listResourceUsersSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listResourceUsers(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { sql, eq, or, inArray, and, count } from "drizzle-orm";
|
|||
import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listResourcesParamsSchema = z
|
||||
.object({
|
||||
|
@ -128,6 +129,34 @@ export type ListResourcesResponse = {
|
|||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/site/{siteId}/resources",
|
||||
description: "List resources for a site.",
|
||||
tags: [OpenAPITags.Site, OpenAPITags.Resource],
|
||||
request: {
|
||||
params: z.object({
|
||||
siteId: z.number()
|
||||
}),
|
||||
query: listResourcesSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/resources",
|
||||
description: "List resources for an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
}),
|
||||
query: listResourcesSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listResources(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { hash } from "@node-rs/argon2";
|
|||
import { response } from "@server/lib";
|
||||
import logger from "@server/logger";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourceAuthMethodsParamsSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
|
||||
|
@ -21,6 +22,25 @@ const setResourceAuthMethodsBodySchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/password",
|
||||
description:
|
||||
"Set the password for a resource. Setting the password to null will remove it.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: setResourceAuthMethodsParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceAuthMethodsBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePassword(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -11,9 +11,10 @@ import { response } from "@server/lib";
|
|||
import stoi from "@server/lib/stoi";
|
||||
import logger from "@server/logger";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourceAuthMethodsParamsSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
|
||||
});
|
||||
|
||||
const setResourceAuthMethodsBodySchema = z
|
||||
|
@ -21,25 +22,44 @@ const setResourceAuthMethodsBodySchema = z
|
|||
pincode: z
|
||||
.string()
|
||||
.regex(/^\d{6}$/)
|
||||
.or(z.null()),
|
||||
.or(z.null())
|
||||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource. Setting the PIN code to null will remove it.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: setResourceAuthMethodsParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceAuthMethodsBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePincode(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = setResourceAuthMethodsParamsSchema.safeParse(
|
||||
req.params,
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString(),
|
||||
),
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -48,8 +68,8 @@ export async function setResourcePincode(
|
|||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString(),
|
||||
),
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -75,15 +95,12 @@ export async function setResourcePincode(
|
|||
success: true,
|
||||
error: false,
|
||||
message: "Resource PIN code set successfully",
|
||||
status: HttpCode.CREATED,
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred",
|
||||
),
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourceRolesBodySchema = z
|
||||
.object({
|
||||
|
@ -24,6 +25,25 @@ const setResourceRolesParamsSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/roles",
|
||||
description:
|
||||
"Set roles for a resource. This will replace all existing roles.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: setResourceRolesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceRolesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourceRoles(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setUserResourcesBodySchema = z
|
||||
.object({
|
||||
|
@ -24,6 +25,25 @@ const setUserResourcesParamsSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/users",
|
||||
description:
|
||||
"Set users for a resource. This will replace all existing users.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.User],
|
||||
request: {
|
||||
params: setUserResourcesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setUserResourcesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourceUsers(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourceWhitelistBodySchema = z
|
||||
.object({
|
||||
|
@ -37,6 +38,25 @@ const setResourceWhitelistParamsSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource. This will replace all existing emails.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: setResourceWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourceWhitelist(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { fromError } from "zod-validation-error";
|
|||
import { addPeer } from "../gerbil/peers";
|
||||
import { addTargets, removeTargets } from "../newt/targets";
|
||||
import { getAllowedIps } from "../target/helpers";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const transferResourceParamsSchema = z
|
||||
.object({
|
||||
|
@ -27,6 +28,25 @@ const transferResourceBodySchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/transfer",
|
||||
description:
|
||||
"Transfer a resource to a different site. This will also transfer the targets associated with the resource.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: transferResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: transferResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function transferResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -17,6 +17,8 @@ import logger from "@server/logger";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import config from "@server/lib/config";
|
||||
import { subdomainSchema } from "@server/lib/schemas";
|
||||
import { registry } from "@server/openApi";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
|
||||
const updateResourceParamsSchema = z
|
||||
.object({
|
||||
|
@ -93,6 +95,26 @@ const updateRawResourceBodySchema = z
|
|||
{ message: "Cannot update proxyPort" }
|
||||
);
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}",
|
||||
description: "Update a resource.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: updateResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateHttpResourceBodySchema.and(
|
||||
updateRawResourceBodySchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
isValidIP,
|
||||
isValidUrlGlobPattern
|
||||
} from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const updateResourceRuleParamsSchema = z
|
||||
|
@ -39,6 +40,24 @@ const updateResourceRuleSchema = z
|
|||
message: "At least one field must be provided for update"
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/rule/{ruleId}",
|
||||
description: "Update a resource rule.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: updateResourceRuleParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateResourceRuleSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateResourceRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -9,6 +9,7 @@ import logger from "@server/logger";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const createRoleParamsSchema = z
|
||||
.object({
|
||||
|
@ -33,6 +34,24 @@ export type CreateRoleBody = z.infer<typeof createRoleSchema>;
|
|||
|
||||
export type CreateRoleResponse = Role;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/role",
|
||||
description: "Create a role.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Role],
|
||||
request: {
|
||||
params: createRoleParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createRoleSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createRole(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteRoleSchema = z
|
||||
.object({
|
||||
|
@ -21,6 +22,24 @@ const deelteRoleBodySchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/role/{roleId}",
|
||||
description: "Delete a role.",
|
||||
tags: [OpenAPITags.Role],
|
||||
request: {
|
||||
params: deleteRoleSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: deelteRoleBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteRole(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { sql, eq } from "drizzle-orm";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listRolesParamsSchema = z
|
||||
.object({
|
||||
|
@ -57,6 +58,18 @@ export type ListRolesResponse = {
|
|||
};
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/orgs/{orgId}/roles",
|
||||
description: "List roles.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Role],
|
||||
request: {
|
||||
params: listRolesParamsSchema,
|
||||
query: listRolesSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listRoles(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { fromError } from "zod-validation-error";
|
|||
import { hash } from "@node-rs/argon2";
|
||||
import { newts } from "@server/db/schemas";
|
||||
import moment from "moment";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
|
||||
const createSiteParamsSchema = z
|
||||
|
@ -35,7 +36,7 @@ const createSiteSchema = z
|
|||
subnet: z.string().optional(),
|
||||
newtId: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
type: z.string()
|
||||
type: z.enum(["newt", "wireguard", "local"])
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
@ -43,6 +44,24 @@ export type CreateSiteBody = z.infer<typeof createSiteSchema>;
|
|||
|
||||
export type CreateSiteResponse = Site;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/site",
|
||||
description: "Create a new site.",
|
||||
tags: [OpenAPITags.Site, OpenAPITags.Org],
|
||||
request: {
|
||||
params: createSiteParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createSiteSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createSite(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -10,6 +10,7 @@ import logger from "@server/logger";
|
|||
import { deletePeer } from "../gerbil/peers";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { sendToClient } from "../ws";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteSiteSchema = z
|
||||
.object({
|
||||
|
@ -17,6 +18,17 @@ const deleteSiteSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/site/{siteId}",
|
||||
description: "Delete a site and all its associated data.",
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: deleteSiteSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteSite(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -9,6 +9,7 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const getSiteSchema = z
|
||||
.object({
|
||||
|
@ -43,6 +44,34 @@ async function query(siteId?: number, niceId?: string, orgId?: string) {
|
|||
|
||||
export type GetSiteResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/site/{niceId}",
|
||||
description:
|
||||
"Get a site by orgId and niceId. NiceId is a readable ID for the site and unique on a per org basis.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Site],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string(),
|
||||
niceId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/site/{siteId}",
|
||||
description: "Get a site by siteId.",
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: z.object({
|
||||
siteId: z.number()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getSite(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import { NextFunction, Request, Response } from "express";
|
|||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listSitesParamsSchema = z
|
||||
.object({
|
||||
|
@ -59,6 +60,18 @@ export type ListSitesResponse = {
|
|||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/sites",
|
||||
description: "List all sites in an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Site],
|
||||
request: {
|
||||
params: listSitesParamsSchema,
|
||||
query: listSitesSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listSites(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -9,6 +9,8 @@ import logger from "@server/logger";
|
|||
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
import config from "@server/lib/config";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { z } from "zod";
|
||||
|
||||
export type PickSiteDefaultsResponse = {
|
||||
exitNodeId: number;
|
||||
|
@ -22,6 +24,20 @@ export type PickSiteDefaultsResponse = {
|
|||
newtSecret: string;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/pick-site-defaults",
|
||||
description:
|
||||
"Return pre-requisite data for creating a site, such as the exit node, subnet, Newt credentials, etc.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Site],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function pickSiteDefaults(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
@ -45,7 +61,7 @@ export async function pickSiteDefaults(
|
|||
// list all of the sites on that exit node
|
||||
const sitesQuery = await db
|
||||
.select({
|
||||
subnet: sites.subnet,
|
||||
subnet: sites.subnet
|
||||
})
|
||||
.from(sites)
|
||||
.where(eq(sites.exitNodeId, exitNode.exitNodeId));
|
||||
|
@ -53,8 +69,17 @@ export async function pickSiteDefaults(
|
|||
// TODO: we need to lock this subnet for some time so someone else does not take it
|
||||
let subnets = sitesQuery.map((site) => site.subnet);
|
||||
// exclude the exit node address by replacing after the / with a site block size
|
||||
subnets.push(exitNode.address.replace(/\/\d+$/, `/${config.getRawConfig().gerbil.site_block_size}`));
|
||||
const newSubnet = findNextAvailableCidr(subnets, config.getRawConfig().gerbil.site_block_size, exitNode.address);
|
||||
subnets.push(
|
||||
exitNode.address.replace(
|
||||
/\/\d+$/,
|
||||
`/${config.getRawConfig().gerbil.site_block_size}`
|
||||
)
|
||||
);
|
||||
const newSubnet = findNextAvailableCidr(
|
||||
subnets,
|
||||
config.getRawConfig().gerbil.site_block_size,
|
||||
exitNode.address
|
||||
);
|
||||
if (!newSubnet) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
@ -77,12 +102,12 @@ export async function pickSiteDefaults(
|
|||
endpoint: exitNode.endpoint,
|
||||
subnet: newSubnet,
|
||||
newtId,
|
||||
newtSecret: secret,
|
||||
newtSecret: secret
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Organization retrieved successfully",
|
||||
status: HttpCode.OK,
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const updateSiteParamsSchema = z
|
||||
.object({
|
||||
|
@ -35,6 +36,25 @@ const updateSiteBodySchema = z
|
|||
message: "At least one field must be provided for update"
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site/{siteId}",
|
||||
description:
|
||||
"Update a site.",
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: updateSiteParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateSiteBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateSite(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { addTargets } from "../newt/targets";
|
|||
import { eq } from "drizzle-orm";
|
||||
import { pickPort } from "./helpers";
|
||||
import { isTargetValid } from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const createTargetParamsSchema = z
|
||||
.object({
|
||||
|
@ -34,6 +35,24 @@ const createTargetSchema = z
|
|||
|
||||
export type CreateTargetResponse = Target;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource/{resourceId}/target",
|
||||
description: "Create a target for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Target],
|
||||
request: {
|
||||
params: createTargetParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createTargetSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createTarget(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { addPeer } from "../gerbil/peers";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { removeTargets } from "../newt/targets";
|
||||
import { getAllowedIps } from "./helpers";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteTargetSchema = z
|
||||
.object({
|
||||
|
@ -18,6 +19,17 @@ const deleteTargetSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/target/{targetId}",
|
||||
description: "Delete a target.",
|
||||
tags: [OpenAPITags.Target],
|
||||
request: {
|
||||
params: deleteTargetSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteTarget(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const getTargetSchema = z
|
||||
.object({
|
||||
|
@ -15,6 +16,17 @@ const getTargetSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/target/{targetId}",
|
||||
description: "Get a target.",
|
||||
tags: [OpenAPITags.Target],
|
||||
request: {
|
||||
params: getTargetSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getTarget(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listTargetsParamsSchema = z
|
||||
.object({
|
||||
|
@ -56,6 +57,18 @@ export type ListTargetsResponse = {
|
|||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/targets",
|
||||
description: "List targets for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Target],
|
||||
request: {
|
||||
params: listTargetsParamsSchema,
|
||||
query: listTargetsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listTargets(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { addPeer } from "../gerbil/peers";
|
|||
import { addTargets } from "../newt/targets";
|
||||
import { pickPort } from "./helpers";
|
||||
import { isTargetValid } from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const updateTargetParamsSchema = z
|
||||
.object({
|
||||
|
@ -31,6 +32,24 @@ const updateTargetBodySchema = z
|
|||
message: "At least one field must be provided for update"
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/target/{targetId}",
|
||||
description: "Update a target.",
|
||||
tags: [OpenAPITags.Target],
|
||||
request: {
|
||||
params: updateTargetParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateTargetBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateTarget(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -9,6 +9,7 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const addUserRoleParamsSchema = z
|
||||
.object({
|
||||
|
@ -19,6 +20,17 @@ const addUserRoleParamsSchema = z
|
|||
|
||||
export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/role/{roleId}/add/{userId}",
|
||||
description: "Add a role to a user.",
|
||||
tags: [OpenAPITags.Role, OpenAPITags.User],
|
||||
request: {
|
||||
params: addUserRoleParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function addUserRole(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -9,6 +9,7 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
async function queryUser(orgId: string, userId: string) {
|
||||
const [user] = await db
|
||||
|
@ -40,6 +41,17 @@ const getOrgUserParamsSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/user/{userId}",
|
||||
description: "Get a user in an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.User],
|
||||
request: {
|
||||
params: getOrgUserParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getOrgUser(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { hashPassword } from "@server/auth/password";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { sendEmail } from "@server/emails";
|
||||
import SendInviteLink from "@server/emails/templates/SendInviteLink";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const inviteUserParamsSchema = z
|
||||
.object({
|
||||
|
@ -42,6 +43,24 @@ export type InviteUserResponse = {
|
|||
|
||||
const inviteTracker: Record<string, { timestamps: number[] }> = {};
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/create-invite",
|
||||
description: "Invite a user to join an organization.",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
params: inviteUserParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: inviteUserBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function inviteUser(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
|||
import { sql } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listUsersParamsSchema = z
|
||||
.object({
|
||||
|
@ -57,6 +58,18 @@ export type ListUsersResponse = {
|
|||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/users",
|
||||
description: "List users in an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.User],
|
||||
request: {
|
||||
params: listUsersParamsSchema,
|
||||
query: listUsersSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listUsers(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const removeUserSchema = z
|
||||
.object({
|
||||
|
@ -16,6 +17,17 @@ const removeUserSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/org/{orgId}/user/{userId}",
|
||||
description: "Remove a user from an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.User],
|
||||
request: {
|
||||
params: removeUserSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function removeUserOrg(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
|
|
@ -53,13 +53,11 @@ const createSiteFormSchema = z
|
|||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: "Name must be at least 2 characters."
|
||||
})
|
||||
.min(2, { message: "Name must be at least 2 characters." })
|
||||
.max(30, {
|
||||
message: "Name must not be longer than 30 characters."
|
||||
}),
|
||||
method: z.string(),
|
||||
method: z.enum(["newt", "wireguard", "local"]),
|
||||
copied: z.boolean()
|
||||
})
|
||||
.refine(
|
||||
|
@ -77,6 +75,15 @@ const createSiteFormSchema = z
|
|||
|
||||
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
|
||||
|
||||
type SiteType = "newt" | "wireguard" | "local";
|
||||
|
||||
interface TunnelTypeOption {
|
||||
id: SiteType;
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type Commands = {
|
||||
mac: Record<string, string[]>;
|
||||
linux: Record<string, string[]>;
|
||||
|
@ -102,7 +109,9 @@ export default function Page() {
|
|||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const [tunnelTypes, setTunnelTypes] = useState<any>([
|
||||
const [tunnelTypes, setTunnelTypes] = useState<
|
||||
ReadonlyArray<TunnelTypeOption>
|
||||
>([
|
||||
{
|
||||
id: "newt",
|
||||
title: "Newt Tunnel (Recommended)",
|
||||
|
@ -342,22 +351,15 @@ WantedBy=default.target`
|
|||
}
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
const form = useForm<CreateSiteFormValues>({
|
||||
resolver: zodResolver(createSiteFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
copied: false,
|
||||
method: "newt"
|
||||
}
|
||||
defaultValues: { name: "", copied: false, method: "newt" }
|
||||
});
|
||||
|
||||
async function onSubmit(data: CreateSiteFormValues) {
|
||||
setCreateLoading(true);
|
||||
|
||||
let payload: CreateSiteBody = {
|
||||
name: data.name,
|
||||
type: data.method
|
||||
};
|
||||
let payload: CreateSiteBody = { name: data.name, type: data.method };
|
||||
|
||||
if (data.method == "wireguard") {
|
||||
if (!siteDefaults || !wgConfig) {
|
||||
|
@ -486,10 +488,7 @@ WantedBy=default.target`
|
|||
|
||||
setTunnelTypes((prev: any) => {
|
||||
return prev.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
disabled: false
|
||||
};
|
||||
return { ...item, disabled: false };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -564,9 +563,8 @@ WantedBy=default.target`
|
|||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
This is the
|
||||
display name for the
|
||||
site.
|
||||
This is the display
|
||||
name for the site.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
|
@ -590,12 +588,10 @@ WantedBy=default.target`
|
|||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={tunnelTypes}
|
||||
defaultValue={
|
||||
form.getValues("method") as string
|
||||
}
|
||||
onChange={(value) =>
|
||||
form.setValue("method", value)
|
||||
}
|
||||
defaultValue={form.getValues("method")}
|
||||
onChange={(value) => {
|
||||
form.setValue("method", value);
|
||||
}}
|
||||
cols={3}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
|
|
46
src/app/components/SupporterMessage.tsx
Normal file
46
src/app/components/SupporterMessage.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
export default function SupporterMessage({ tier }: { tier: string }) {
|
||||
return (
|
||||
<div className="relative flex items-center space-x-2 whitespace-nowrap group">
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => {
|
||||
// Get the bounding box of the element
|
||||
const rect = (
|
||||
e.target as HTMLElement
|
||||
).getBoundingClientRect();
|
||||
|
||||
// Trigger confetti centered on the word "Pangolin"
|
||||
confetti({
|
||||
particleCount: 100,
|
||||
spread: 70,
|
||||
origin: {
|
||||
x: (rect.left + rect.width / 2) / window.innerWidth,
|
||||
y: rect.top / window.innerHeight
|
||||
},
|
||||
colors: ["#FFA500", "#FF4500", "#FFD700"]
|
||||
});
|
||||
}}
|
||||
>
|
||||
Pangolin
|
||||
</span>
|
||||
{/* SVG Star */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4 text-primary"
|
||||
>
|
||||
<path d="M12 .587l3.668 7.431 8.2 1.192-5.934 5.782 1.4 8.168L12 18.896l-7.334 3.864 1.4-8.168L.132 9.21l8.2-1.192z" />
|
||||
</svg>
|
||||
{/* Popover */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block bg-white/10 text-primary text-sm rounded-md shadow-lg px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Thank you for supporting Pangolin as a {tier}!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,20 +2,19 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 0.0% 10.0%;
|
||||
--foreground: 20 0% 10%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 0.0% 10.0%;
|
||||
--card-foreground: 20 0% 10%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 0.0% 10.0%;
|
||||
--popover-foreground: 20 0% 10%;
|
||||
--primary: 24.6 95% 53.1%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
--muted: 60 4.8% 85.0%;
|
||||
--muted: 60 4.8% 85%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
--accent: 60 4.8% 90%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
@ -33,24 +32,24 @@
|
|||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 0.0% 10.0%;
|
||||
--background: 20 0% 10%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
--card: 20 0.0% 10.0%;
|
||||
--card: 20 0% 10%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
--popover: 20 0.0% 10.0%;
|
||||
--popover: 20 0% 10%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
--primary: 20.5 90.2% 48.2%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 12 6.5% 15.0%;
|
||||
--secondary: 12 6.5% 15%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
--muted: 12 6.5% 25.0%;
|
||||
--muted: 12 6.5% 25%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
--accent: 12 2.5% 15.0%;
|
||||
--accent: 12 2.5% 15%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--border: 12 6.5% 30.0%;
|
||||
--input: 12 6.5% 35.0%;
|
||||
--border: 12 6.5% 30%;
|
||||
--input: 12 6.5% 35%;
|
||||
--ring: 20.5 90.2% 48.2%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
|
@ -60,7 +59,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
|
@ -70,4 +68,3 @@
|
|||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,13 +12,14 @@ import SupportStatusProvider from "@app/providers/SupporterStatusProvider";
|
|||
import { createApiClient, internal, priv } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
|
||||
import SupporterMessage from "./components/SupporterMessage";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Dashboard - Pangolin`,
|
||||
description: ""
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// const font = Figtree({ subsets: ["latin"] });
|
||||
const font = Inter({ subsets: ["latin"] });
|
||||
|
@ -34,9 +35,9 @@ export default async function RootLayout({
|
|||
visible: true
|
||||
} as any;
|
||||
|
||||
const res = await priv.get<
|
||||
AxiosResponse<IsSupporterKeyVisibleResponse>
|
||||
>("supporter-key/visible");
|
||||
const res = await priv.get<AxiosResponse<IsSupporterKeyVisibleResponse>>(
|
||||
"supporter-key/visible"
|
||||
);
|
||||
supporterData.visible = res.data.data.visible;
|
||||
supporterData.tier = res.data.data.tier;
|
||||
|
||||
|
@ -61,9 +62,15 @@ export default async function RootLayout({
|
|||
{/* Footer */}
|
||||
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
|
||||
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600">
|
||||
{supporterData?.tier ? (
|
||||
<SupporterMessage
|
||||
tier={supporterData.tier}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<span>Pangolin</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator orientation="vertical" />
|
||||
<a
|
||||
href="https://fossorial.io/"
|
||||
|
|
|
@ -4,38 +4,39 @@ import { cn } from "@app/lib/cn";
|
|||
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
||||
import { useState } from "react";
|
||||
|
||||
interface StrategyOption {
|
||||
id: string;
|
||||
interface StrategyOption<TValue extends string> {
|
||||
id: TValue;
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean; // New optional property
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface StrategySelectProps {
|
||||
options: StrategyOption[];
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
interface StrategySelectProps<TValue extends string> {
|
||||
options: ReadonlyArray<StrategyOption<TValue>>;
|
||||
defaultValue?: TValue;
|
||||
onChange?: (value: TValue) => void;
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
export function StrategySelect({
|
||||
export function StrategySelect<TValue extends string>({
|
||||
options,
|
||||
defaultValue,
|
||||
onChange,
|
||||
cols
|
||||
}: StrategySelectProps) {
|
||||
const [selected, setSelected] = useState(defaultValue);
|
||||
}: StrategySelectProps<TValue>) {
|
||||
const [selected, setSelected] = useState<TValue | undefined>(defaultValue);
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={(value) => {
|
||||
setSelected(value);
|
||||
onChange?.(value);
|
||||
onValueChange={(value: string) => {
|
||||
const typedValue = value as TValue;
|
||||
setSelected(typedValue);
|
||||
onChange?.(typedValue);
|
||||
}}
|
||||
className={`grid md:grid-cols-${cols ? cols : 1} gap-4`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
{options.map((option: StrategyOption<TValue>) => (
|
||||
<label
|
||||
key={option.id}
|
||||
htmlFor={option.id}
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
CardTitle
|
||||
} from "./ui/card";
|
||||
import { Check, ExternalLink } from "lucide-react";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
const formSchema = z.object({
|
||||
githubUsername: z
|
||||
|
@ -100,6 +101,7 @@ export default function SupporterStatus() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Trigger the toast
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Valid Key",
|
||||
|
@ -107,6 +109,50 @@ export default function SupporterStatus() {
|
|||
"Your supporter key has been validated. Thank you for your support!"
|
||||
});
|
||||
|
||||
// Fireworks-style confetti
|
||||
const duration = 5 * 1000; // 5 seconds
|
||||
const animationEnd = Date.now() + duration;
|
||||
const defaults = {
|
||||
startVelocity: 30,
|
||||
spread: 360,
|
||||
ticks: 60,
|
||||
zIndex: 0,
|
||||
colors: ["#FFA500", "#FF4500", "#FFD700"] // Orange hues
|
||||
};
|
||||
|
||||
function randomInRange(min: number, max: number) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const timeLeft = animationEnd - Date.now();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
const particleCount = 50 * (timeLeft / duration);
|
||||
|
||||
// Launch confetti from two random horizontal positions
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: {
|
||||
x: randomInRange(0.1, 0.3),
|
||||
y: Math.random() - 0.2
|
||||
}
|
||||
});
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: {
|
||||
x: randomInRange(0.7, 0.9),
|
||||
y: Math.random() - 0.2
|
||||
}
|
||||
});
|
||||
}, 250);
|
||||
|
||||
setPurchaseOptionsOpen(false);
|
||||
setKeyOpen(false);
|
||||
|
||||
|
@ -177,7 +223,9 @@ export default function SupporterStatus() {
|
|||
</p>
|
||||
|
||||
<div className="py-6">
|
||||
<p className="mb-3 text-center">Please select the option that best suits you.</p>
|
||||
<p className="mb-3 text-center">
|
||||
Please select the option that best suits you.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 grid-cols-1 gap-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
19
src/types/canvas-confetti.d.ts
vendored
Normal file
19
src/types/canvas-confetti.d.ts
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
declare module "canvas-confetti" {
|
||||
export interface ConfettiOptions {
|
||||
particleCount?: number;
|
||||
angle?: number;
|
||||
spread?: number;
|
||||
startVelocity?: number;
|
||||
decay?: number;
|
||||
gravity?: number;
|
||||
drift?: number;
|
||||
ticks?: number;
|
||||
origin?: { x?: number; y?: number };
|
||||
colors?: string[];
|
||||
shapes?: string[];
|
||||
scalar?: number;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export default function confetti(options?: ConfettiOptions): Promise<null>;
|
||||
}
|
|
@ -5,59 +5,59 @@ const config: Config = {
|
|||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))"
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))"
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))"
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))"
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))"
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))"
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))"
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
"1": "hsl(var(--chart-1))",
|
||||
"2": "hsl(var(--chart-2))",
|
||||
"3": "hsl(var(--chart-3))",
|
||||
"4": "hsl(var(--chart-4))",
|
||||
"5": "hsl(var(--chart-5))"
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)"
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [require("tailwindcss-animate")]
|
||||
};
|
||||
export default config;
|
||||
|
|
Loading…
Reference in a new issue