Merge pull request #161 from Dokploy/canary

v0.2.4
This commit is contained in:
Mauricio Siu
2024-06-23 19:40:45 -06:00
committed by GitHub
42 changed files with 9895 additions and 101 deletions
@@ -10,7 +10,7 @@ import { RedbuildApplication } from "../rebuild-application";
import { StartApplication } from "../start-application"; import { StartApplication } from "../start-application";
import { StopApplication } from "../stop-application"; import { StopApplication } from "../stop-application";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { Terminal } from "lucide-react"; import { CheckCircle2, Terminal } from "lucide-react";
import { DeployApplication } from "./deploy-application"; import { DeployApplication } from "./deploy-application";
import { ResetApplication } from "./reset-application"; import { ResetApplication } from "./reset-application";
interface Props { interface Props {
@@ -55,8 +55,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error to update Auto Deploy"); toast.error("Error to update Auto Deploy");
}); });
}} }}
className="flex flex-row gap-2 items-center"
> >
Autodeploy Autodeploy
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
</Toggle> </Toggle>
<RedbuildApplication applicationId={applicationId} /> <RedbuildApplication applicationId={applicationId} />
{data?.applicationStatus === "idle" ? ( {data?.applicationStatus === "idle" ? (
@@ -0,0 +1,73 @@
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
placeholder="Token"
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>
);
};
@@ -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 || "",
-1
View File
@@ -32,7 +32,6 @@ export const ShowSettings = () => {
<ShowCertificates /> <ShowCertificates />
<WebDomain /> <WebDomain />
<WebServer /> <WebServer />
<ShowUsers /> <ShowUsers />
</> </>
)} )}
@@ -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"
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "auth" ADD COLUMN "token" text;
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "canAccessToAPI" boolean DEFAULT false NOT NULL;
+1
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
+21
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
} }
] ]
} }
+4 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.2.3", "version": "v0.2.4",
"private": true, "private": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "module", "type": "module",
@@ -109,9 +109,11 @@
"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",
"@dokploy/trpc-openapi": "0.0.4",
"use-resize-observer": "9.1.0", "use-resize-observer": "9.1.0",
"ws": "8.16.0", "ws": "8.16.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
@@ -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",
+10 -10
View File
@@ -5,14 +5,14 @@ import { createTRPCContext } from "@/server/api/trpc";
// export API handler // export API handler
export default createNextApiHandler({ export default createNextApiHandler({
router: appRouter, router: appRouter,
createContext: createTRPCContext, createContext: createTRPCContext,
onError: onError:
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? ({ path, error }) => { ? ({ path, error }) => {
console.error( console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`, `❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
); );
} }
: undefined, : undefined,
}); });
+12
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
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: {},
};
}
+1593 -4
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -23,6 +23,7 @@ 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";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
* *
@@ -39,6 +40,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,7 +52,6 @@ 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,
}); });
+21
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;
}),
}); });
+6 -1
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 }) => {
+48 -1
View File
@@ -41,6 +41,8 @@ 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 { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { appRouter } from "../root";
export const settingsRouter = createTRPCRouter({ export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => { reloadServer: adminProcedure.mutation(async () => {
@@ -242,5 +244,50 @@ export const settingsRouter = createTRPCRouter({
} }
return readConfigInPath(input.path); return readConfigInPath(input.path);
}), }),
getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => {
const protocol = ctx.req.headers["x-forwarded-proto"];
const url = `${protocol}://${ctx.req.headers.host}/api/trpc`;
const openApiDocument = generateOpenApiDocument(appRouter, {
title: "tRPC OpenAPI",
version: "1.0.0",
baseUrl: url,
docsUrl: `${url}/settings.getOpenApiDocument`,
tags: [
"admin",
"docker",
"compose",
"registry",
"cluster",
"user",
"domain",
"destination",
"backup",
"deployment",
"mounts",
"certificates",
"settings",
"security",
"redirects",
"port",
"project",
"application",
"mysql",
"postgres",
"redis",
"mongo",
"mariadb",
],
});
openApiDocument.info = {
title: "Dokploy API",
description: "Endpoints for dokploy",
version: getDokployVersion(),
};
return openApiDocument;
},
),
}); });
// apt-get install apache2-utils
+4
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);
+4
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);
+4
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);
+4
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);
+5
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);
+4
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);
+4
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);
+41 -16
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 { validateBearerToken } from "../auth/token";
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
/** /**
* 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,19 +96,22 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* errors on the backend. * errors on the backend.
*/ */
const t = initTRPC.context<typeof createTRPCContext>().create({ const t = initTRPC
transformer: superjson, .meta<OpenApiMeta>()
errorFormatter({ shape, error }) { .context<typeof createTRPCContext>()
return { .create({
...shape, transformer: superjson,
data: { errorFormatter({ shape, error }) {
...shape.data, return {
zodError: ...shape,
error.cause instanceof ZodError ? error.cause.flatten() : null, data: {
}, ...shape.data,
}; zodError:
}, error.cause instanceof ZodError ? error.cause.flatten() : null,
}); },
};
},
});
/** /**
* 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" });
+7 -4
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
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,
}),
};
};
+6 -11
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({
+1
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()
+7 -13
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({
+1 -6
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({
+1 -6
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({
+1 -6
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({
+1 -6
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({
+1 -6
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({
-7
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")
+2
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();
+11
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;
}
}