mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #158 from Dokploy/57-dokploy-api-or-cli
57 dokploy api or cli
This commit is contained in:
commit
b360cc2af4
69
components/dashboard/settings/profile/generate-token.tsx
Normal file
69
components/dashboard/settings/profile/generate-token.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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 || "",
|
||||||
|
@ -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
drizzle/0015_fearless_callisto.sql
Normal file
1
drizzle/0015_fearless_callisto.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "auth" ADD COLUMN "token" text;
|
1
drizzle/0016_chunky_leopardon.sql
Normal file
1
drizzle/0016_chunky_leopardon.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "canAccessToAPI" boolean DEFAULT false NOT NULL;
|
1
drizzle/0017_yummy_norrin_radd.sql
Normal file
1
drizzle/0017_yummy_norrin_radd.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ALTER COLUMN "token" DROP NOT NULL;
|
2613
drizzle/meta/0015_snapshot.json
Normal file
2613
drizzle/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2620
drizzle/meta/0016_snapshot.json
Normal file
2620
drizzle/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2620
drizzle/meta/0017_snapshot.json
Normal file
2620
drizzle/meta/0017_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -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",
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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
63
pages/swagger.tsx
Normal 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: {},
|
||||||
|
};
|
||||||
|
}
|
1275
pnpm-lock.yaml
1275
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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: [] }];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,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<OperationMeta>()
|
||||||
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" });
|
||||||
|
@ -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
48
server/auth/token.ts
Normal 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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -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({
|
||||||
|
@ -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()
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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")
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user