Merge pull request #158 from Dokploy/57-dokploy-api-or-cli

57 dokploy api or cli
This commit is contained in:
Mauricio Siu 2024-06-23 00:28:39 -06:00 committed by GitHub
commit b360cc2af4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 9545 additions and 97 deletions

View File

@ -0,0 +1,69 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { ExternalLinkIcon } from "lucide-react";
export const GenerateToken = () => {
const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation();
return (
<Card className="bg-transparent">
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div>
<CardTitle className="text-xl">API/CLI</CardTitle>
<CardDescription>
Generate a token to access the API/CLI
</CardDescription>
</div>
<div className="flex flex-row gap-2 max-sm:flex-wrap items-end">
<span className="text-sm font-medium text-muted-foreground">
Swagger API:
</span>
<Link
href="/swagger"
target="_blank"
className="flex flex-row gap-2 items-center"
>
<span className="text-sm font-medium">View</span>
<ExternalLinkIcon className="size-4" />
</Link>
</div>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex flex-row gap-2 max-sm:flex-wrap justify-end items-end">
<div className="grid w-full gap-8">
<div className="flex flex-col gap-2">
<Label>Token</Label>
<ToggleVisibilityInput value={data?.token || ""} disabled />
</div>
</div>
<Button
type="button"
isLoading={isLoadingToken}
onClick={async () => {
await generateToken().then(() => {
refetch();
toast.success("Token generated");
});
}}
>
Generate
</Button>
</div>
</CardContent>
</Card>
);
};

View File

@ -51,6 +51,9 @@ const randomImages = [
export const ProfileForm = () => { export const ProfileForm = () => {
const { data, refetch } = api.auth.get.useQuery(); const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync, isLoading } = api.auth.update.useMutation(); const { mutateAsync, isLoading } = api.auth.update.useMutation();
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation();
const form = useForm<Profile>({ const form = useForm<Profile>({
defaultValues: { defaultValues: {
email: data?.email || "", email: data?.email || "",

View File

@ -32,7 +32,6 @@ export const ShowSettings = () => {
<ShowCertificates /> <ShowCertificates />
<WebDomain /> <WebDomain />
<WebServer /> <WebServer />
<ShowUsers /> <ShowUsers />
</> </>
)} )}

View File

@ -38,6 +38,7 @@ const addPermissions = z.object({
canDeleteServices: z.boolean().optional().default(false), canDeleteServices: z.boolean().optional().default(false),
canAccessToTraefikFiles: z.boolean().optional().default(false), canAccessToTraefikFiles: z.boolean().optional().default(false),
canAccessToDocker: z.boolean().optional().default(false), canAccessToDocker: z.boolean().optional().default(false),
canAccessToAPI: z.boolean().optional().default(false),
}); });
type AddPermissions = z.infer<typeof addPermissions>; type AddPermissions = z.infer<typeof addPermissions>;
@ -80,6 +81,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canDeleteServices: data.canDeleteServices, canDeleteServices: data.canDeleteServices,
canAccessToTraefikFiles: data.canAccessToTraefikFiles, canAccessToTraefikFiles: data.canAccessToTraefikFiles,
canAccessToDocker: data.canAccessToDocker, canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
}); });
} }
}, [form, form.formState.isSubmitSuccessful, form.reset, data]); }, [form, form.formState.isSubmitSuccessful, form.reset, data]);
@ -95,6 +97,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
accesedProjects: data.accesedProjects || [], accesedProjects: data.accesedProjects || [],
accesedServices: data.accesedServices || [], accesedServices: data.accesedServices || [],
canAccessToDocker: data.canAccessToDocker, canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
}) })
.then(async () => { .then(async () => {
toast.success("Permissions updated"); toast.success("Permissions updated");
@ -247,6 +250,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="canAccessToAPI"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Access to API/CLI</FormLabel>
<FormDescription>
Allow the user to access to the API/CLI
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="accesedProjects" name="accesedProjects"

View File

@ -0,0 +1 @@
ALTER TABLE "auth" ADD COLUMN "token" text;

View File

@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "canAccessToAPI" boolean DEFAULT false NOT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE "user" ALTER COLUMN "token" DROP NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -106,6 +106,27 @@
"when": 1716715367982, "when": 1716715367982,
"tag": "0014_same_hammerhead", "tag": "0014_same_hammerhead",
"breakpoints": true "breakpoints": true
},
{
"idx": 15,
"version": "6",
"when": 1717564517104,
"tag": "0015_fearless_callisto",
"breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1719109196484,
"tag": "0016_chunky_leopardon",
"breakpoints": true
},
{
"idx": 17,
"version": "6",
"when": 1719109531147,
"tag": "0017_yummy_norrin_radd",
"breakpoints": true
} }
] ]
} }

View File

@ -98,6 +98,7 @@
"node-pty": "1.0.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"octokit": "3.1.2", "octokit": "3.1.2",
"openapi-trpc": "^0.2.0",
"otpauth": "^9.2.3", "otpauth": "^9.2.3",
"postgres": "3.4.4", "postgres": "3.4.4",
"public-ip": "6.0.2", "public-ip": "6.0.2",
@ -109,6 +110,7 @@
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.4.0", "sonner": "^1.4.0",
"superjson": "^2.2.1", "superjson": "^2.2.1",
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tar-fs": "3.0.5", "tar-fs": "3.0.5",
@ -129,6 +131,7 @@
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37", "@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^18.2.15",
"@types/swagger-ui-react": "^4.18.3",
"@types/tar-fs": "2.0.4", "@types/tar-fs": "2.0.4",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",

View File

@ -1,14 +1,26 @@
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form"; import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
const Page = () => { const Page = () => {
const { data } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
return ( return (
<div className="flex flex-col gap-4 w-full"> <div className="flex flex-col gap-4 w-full">
<ProfileForm /> <ProfileForm />
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
</div> </div>
); );
}; };

63
pages/swagger.tsx Normal file
View File

@ -0,0 +1,63 @@
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext, NextPage } from "next";
import dynamic from "next/dynamic";
import "swagger-ui-react/swagger-ui.css";
import superjson from "superjson";
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
const Home: NextPage = () => {
const { data } = api.settings.getOpenApiDocument.useQuery();
return (
<div className="h-screen bg-white">
<SwaggerUI spec={data || {}} />
</div>
);
};
export default Home;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context;
const { user, session } = await validateRequest(context.req, context.res);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
if (user.rol === "user") {
const result = await helpers.user.byAuthId.fetch({
authId: user.id,
});
if (!result.canAccessToAPI) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
}
return {
props: {},
};
}

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,8 @@ import { dockerRouter } from "./routers/docker";
import { composeRouter } from "./routers/compose"; import { composeRouter } from "./routers/compose";
import { registryRouter } from "./routers/registry"; import { registryRouter } from "./routers/registry";
import { clusterRouter } from "./routers/cluster"; import { clusterRouter } from "./routers/cluster";
import { generateOpenAPIDocumentFromTRPCRouter } from "openapi-trpc";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
* *
@ -39,6 +41,7 @@ export const appRouter = createTRPCRouter({
redis: redisRouter, redis: redisRouter,
mongo: mongoRouter, mongo: mongoRouter,
mariadb: mariadbRouter, mariadb: mariadbRouter,
compose: composeRouter,
user: userRouter, user: userRouter,
domain: domainRouter, domain: domainRouter,
destination: destinationRouter, destination: destinationRouter,
@ -50,10 +53,15 @@ export const appRouter = createTRPCRouter({
security: securityRouter, security: securityRouter,
redirects: redirectsRouter, redirects: redirectsRouter,
port: portRouter, port: portRouter,
compose: composeRouter,
registry: registryRouter, registry: registryRouter,
cluster: clusterRouter, cluster: clusterRouter,
}); });
// export type definition of API // export type definition of API
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;
export const doc = generateOpenAPIDocumentFromTRPCRouter(appRouter, {
pathPrefix: "/api/trpc",
processOperation(op) {
op.security = [{ bearerAuth: [] }];
},
});

View File

@ -26,6 +26,7 @@ import {
updateAuthById, updateAuthById,
verify2FA, verify2FA,
} from "../services/auth"; } from "../services/auth";
import { luciaToken } from "@/server/auth/token";
export const authRouter = createTRPCRouter({ export const authRouter = createTRPCRouter({
createAdmin: publicProcedure createAdmin: publicProcedure
@ -138,6 +139,23 @@ export const authRouter = createTRPCRouter({
return auth; return auth;
}), }),
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
if (auth.token) {
await luciaToken.invalidateSession(auth.token);
}
const session = await luciaToken.createSession(auth?.id || "", {
expiresIn: 60 * 60 * 24 * 30,
});
await updateAuthById(auth.id, {
token: session.id,
});
return auth;
}),
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => { one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
const auth = await findAuthById(input.id); const auth = await findAuthById(input.id);
return auth; return auth;
@ -196,4 +214,7 @@ export const authRouter = createTRPCRouter({
}); });
return auth; return auth;
}), }),
verifyToken: protectedProcedure.mutation(async () => {
return true;
}),
}); });

View File

@ -1,4 +1,8 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import {
cliProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiCreateProject, apiCreateProject,
@ -54,6 +58,7 @@ export const projectRouter = createTRPCRouter({
}); });
} }
}), }),
one: protectedProcedure one: protectedProcedure
.input(apiFindOneProject) .input(apiFindOneProject)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {

View File

@ -41,6 +41,7 @@ import {
} from "../services/settings"; } from "../services/settings";
import { canAccessToTraefikFiles } from "../services/user"; import { canAccessToTraefikFiles } from "../services/user";
import { recreateDirectory } from "@/server/utils/filesystem/directory"; import { recreateDirectory } from "@/server/utils/filesystem/directory";
import { doc } from "../root";
export const settingsRouter = createTRPCRouter({ export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => { reloadServer: adminProcedure.mutation(async () => {
@ -242,5 +243,22 @@ export const settingsRouter = createTRPCRouter({
} }
return readConfigInPath(input.path); return readConfigInPath(input.path);
}), }),
getOpenApiDocument: protectedProcedure.query((): unknown => {
doc.components = {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
};
doc.info = {
title: "Dokploy API",
description: "Endpoints for dokploy",
version: getDokployVersion(),
};
return doc;
}),
}); });
// apt-get install apache2-utils

View File

@ -16,11 +16,15 @@ import { createTraefikConfig } from "@/server/utils/traefik/application";
import { docker } from "@/server/constants"; import { docker } from "@/server/constants";
import { getAdvancedStats } from "@/server/monitoring/utilts"; import { getAdvancedStats } from "@/server/monitoring/utilts";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type Application = typeof applications.$inferSelect; export type Application = typeof applications.$inferSelect;
export const createApplication = async ( export const createApplication = async (
input: typeof apiCreateApplication._type, input: typeof apiCreateApplication._type,
) => { ) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
if (input.appName) { if (input.appName) {
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(input.appName);

View File

@ -14,10 +14,14 @@ import { COMPOSE_PATH } from "@/server/constants";
import { cloneGithubRepository } from "@/server/utils/providers/github"; import { cloneGithubRepository } from "@/server/utils/providers/github";
import { cloneGitRepository } from "@/server/utils/providers/git"; import { cloneGitRepository } from "@/server/utils/providers/git";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Compose = typeof compose.$inferSelect; export type Compose = typeof compose.$inferSelect;
export const createCompose = async (input: typeof apiCreateCompose._type) => { export const createCompose = async (input: typeof apiCreateCompose._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
if (input.appName) { if (input.appName) {
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(input.appName);

View File

@ -6,10 +6,14 @@ import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Mariadb = typeof mariadb.$inferSelect; export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
if (input.appName) { if (input.appName) {
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(input.appName);

View File

@ -6,10 +6,14 @@ import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Mongo = typeof mongo.$inferSelect; export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => { export const createMongo = async (input: typeof apiCreateMongo._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
if (input.appName) { if (input.appName) {
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(input.appName);

View File

@ -6,10 +6,15 @@ import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type MySql = typeof mysql.$inferSelect; export type MySql = typeof mysql.$inferSelect;
export const createMysql = async (input: typeof apiCreateMySql._type) => { export const createMysql = async (input: typeof apiCreateMySql._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
if (input.appName) { if (input.appName) {
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(input.appName);

View File

@ -6,10 +6,14 @@ import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type Postgres = typeof postgres.$inferSelect; export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => { export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
if (input.appName) { if (input.appName) {
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(input.appName);

View File

@ -6,11 +6,15 @@ import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Redis = typeof redis.$inferSelect; export type Redis = typeof redis.$inferSelect;
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881 // https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
export const createRedis = async (input: typeof apiCreateRedis._type) => { export const createRedis = async (input: typeof apiCreateRedis._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
if (input.appName) { if (input.appName) {
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(input.appName);

View File

@ -15,6 +15,8 @@ import superjson from "superjson";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { validateRequest } from "../auth/auth"; import { validateRequest } from "../auth/auth";
import type { Session, User } from "lucia"; import type { Session, User } from "lucia";
import type { OperationMeta } from "openapi-trpc";
import { validateBearerToken } from "../auth/token";
/** /**
* 1. CONTEXT * 1. CONTEXT
@ -59,9 +61,15 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
*/ */
export const createTRPCContext = async (opts: CreateNextContextOptions) => { export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts; const { req, res } = opts;
// const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
const { session, user } = await validateRequest(req, res); let { session, user } = await validateBearerToken(req);
user;
if (!session) {
const cookieResult = await validateRequest(req, res);
session = cookieResult.session;
user = cookieResult.user;
}
return createInnerTRPCContext({ return createInnerTRPCContext({
req, req,
res, res,
@ -88,7 +96,10 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* errors on the backend. * errors on the backend.
*/ */
const t = initTRPC.context<typeof createTRPCContext>().create({ const t = initTRPC
.meta<OperationMeta>()
.context<typeof createTRPCContext>()
.create({
transformer: superjson, transformer: superjson,
errorFormatter({ shape, error }) { errorFormatter({ shape, error }) {
return { return {
@ -100,7 +111,7 @@ const t = initTRPC.context<typeof createTRPCContext>().create({
}, },
}; };
}, },
}); });
/** /**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
@ -147,6 +158,20 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
}); });
}); });
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: ctx.session,
user: ctx.user,
// session: { ...ctx.session, user: ctx.user },
},
});
});
export const adminProcedure = t.procedure.use(({ ctx, next }) => { export const adminProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") { if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
throw new TRPCError({ code: "UNAUTHORIZED" }); throw new TRPCError({ code: "UNAUTHORIZED" });

View File

@ -33,14 +33,17 @@ declare module "lucia" {
} }
} }
export type ReturnValidateToken = Promise<{
user: (User & { authId: string }) | null;
session: Session | null;
}>;
export async function validateRequest( export async function validateRequest(
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse, res: ServerResponse,
): Promise<{ ): ReturnValidateToken {
user: (User & { authId: string }) | null;
session: Session | null;
}> {
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
if (!sessionId) { if (!sessionId) {
return { return {
user: null, user: null,

48
server/auth/token.ts Normal file
View File

@ -0,0 +1,48 @@
import { Lucia } from "lucia/dist/core.js";
import type { IncomingMessage } from "node:http";
import { TimeSpan } from "lucia";
import { adapter, type ReturnValidateToken } from "./auth";
export const luciaToken = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: false,
},
},
sessionExpiresIn: new TimeSpan(365, "d"),
getUserAttributes: (attributes) => {
return {
email: attributes.email,
rol: attributes.rol,
secret: attributes.secret !== null,
};
},
});
export const validateBearerToken = async (
req: IncomingMessage,
): ReturnValidateToken => {
const authorizationHeader = req.headers.authorization;
const sessionId = luciaToken.readBearerToken(authorizationHeader ?? "");
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await luciaToken.validateSession(sessionId);
return {
session: result.session,
...((result.user && {
user: {
authId: result.user.id,
email: result.user.email,
rol: result.user.rol,
id: result.user.id,
secret: result.user.secret,
},
}) || {
user: null,
}),
};
};

View File

@ -308,17 +308,12 @@ const createSchema = createInsertSchema(applications, {
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
}); });
export const apiCreateApplication = createSchema export const apiCreateApplication = createSchema.pick({
.pick({
name: true, name: true,
appName: true, appName: true,
description: true, description: true,
projectId: true, projectId: true,
}) });
.transform((data) => ({
...data,
appName: `${data.appName}-${generatePassword(6)}` || generateAppName("app"),
}));
export const apiFindOneApplication = createSchema export const apiFindOneApplication = createSchema
.pick({ .pick({

View File

@ -43,6 +43,7 @@ export const auth = pgTable("auth", {
rol: roles("rol").notNull(), rol: roles("rol").notNull(),
image: text("image").$defaultFn(() => generateRandomImage()), image: text("image").$defaultFn(() => generateRandomImage()),
secret: text("secret"), secret: text("secret"),
token: text("token"),
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false), is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()

View File

@ -75,19 +75,13 @@ const createSchema = createInsertSchema(compose, {
composeType: z.enum(["docker-compose", "stack"]).optional(), composeType: z.enum(["docker-compose", "stack"]).optional(),
}); });
export const apiCreateCompose = createSchema export const apiCreateCompose = createSchema.pick({
.pick({
name: true, name: true,
description: true, description: true,
projectId: true, projectId: true,
composeType: true, composeType: true,
appName: true, appName: true,
}) });
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("compose"),
}));
export const apiCreateComposeByTemplate = createSchema export const apiCreateComposeByTemplate = createSchema
.pick({ .pick({

View File

@ -89,12 +89,7 @@ export const apiCreateMariaDB = createSchema
databaseUser: true, databaseUser: true,
databasePassword: true, databasePassword: true,
}) })
.required() .required();
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("mariadb"),
}));
export const apiFindOneMariaDB = createSchema export const apiFindOneMariaDB = createSchema
.pick({ .pick({

View File

@ -81,12 +81,7 @@ export const apiCreateMongo = createSchema
databaseUser: true, databaseUser: true,
databasePassword: true, databasePassword: true,
}) })
.required() .required();
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
}));
export const apiFindOneMongo = createSchema export const apiFindOneMongo = createSchema
.pick({ .pick({

View File

@ -87,12 +87,7 @@ export const apiCreateMySql = createSchema
databasePassword: true, databasePassword: true,
databaseRootPassword: true, databaseRootPassword: true,
}) })
.required() .required();
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("mysql"),
}));
export const apiFindOneMySql = createSchema export const apiFindOneMySql = createSchema
.pick({ .pick({

View File

@ -83,12 +83,7 @@ export const apiCreatePostgres = createSchema
projectId: true, projectId: true,
description: true, description: true,
}) })
.required() .required();
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
}));
export const apiFindOnePostgres = createSchema export const apiFindOnePostgres = createSchema
.pick({ .pick({

View File

@ -76,12 +76,7 @@ export const apiCreateRedis = createSchema
projectId: true, projectId: true,
description: true, description: true,
}) })
.required() .required();
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("redis"),
}));
export const apiFindOneRedis = createSchema export const apiFindOneRedis = createSchema
.pick({ .pick({

View File

@ -1,13 +1,6 @@
import { auth } from "./auth"; import { auth } from "./auth";
import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
// export const sessionTable = sqliteTable("session", {
// id: text("id").notNull().primaryKey(),
// userId: text("user_id")
// .notNull()
// .references(() => users.id),
// expiresAt: integer("expires_at").notNull(),
// });
export const sessionTable = pgTable("session", { export const sessionTable = pgTable("session", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
userId: text("user_id") userId: text("user_id")

View File

@ -32,6 +32,7 @@ export const users = pgTable("user", {
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false), canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
canDeleteServices: boolean("canDeleteServices").notNull().default(false), canDeleteServices: boolean("canDeleteServices").notNull().default(false),
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false), canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles") canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull() .notNull()
.default(false), .default(false),
@ -105,6 +106,7 @@ export const apiAssignPermissions = createSchema
accesedServices: true, accesedServices: true,
canAccessToTraefikFiles: true, canAccessToTraefikFiles: true,
canAccessToDocker: true, canAccessToDocker: true,
canAccessToAPI: true,
}) })
.required(); .required();

View File

@ -158,3 +158,14 @@
.animate-heartbeat { .animate-heartbeat {
animation: heartbeat 2.5s infinite; animation: heartbeat 2.5s infinite;
} }
@media (prefers-color-scheme: dark) {
.swagger-ui {
background-color: white;
}
.swagger-ui .info{
margin: 0px !important;
padding-top: 1rem !important;
}
}