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
41 changed files with 9545 additions and 97 deletions
@@ -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 = () => {
const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync, isLoading } = api.auth.update.useMutation();
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation();
const form = useForm<Profile>({
defaultValues: {
email: data?.email || "",
-1
View File
@@ -32,7 +32,6 @@ export const ShowSettings = () => {
<ShowCertificates />
<WebDomain />
<WebServer />
<ShowUsers />
</>
)}
@@ -38,6 +38,7 @@ const addPermissions = z.object({
canDeleteServices: z.boolean().optional().default(false),
canAccessToTraefikFiles: z.boolean().optional().default(false),
canAccessToDocker: z.boolean().optional().default(false),
canAccessToAPI: z.boolean().optional().default(false),
});
type AddPermissions = z.infer<typeof addPermissions>;
@@ -80,6 +81,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canDeleteServices: data.canDeleteServices,
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
});
}
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
@@ -95,6 +97,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
accesedProjects: data.accesedProjects || [],
accesedServices: data.accesedServices || [],
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
})
.then(async () => {
toast.success("Permissions updated");
@@ -247,6 +250,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
</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
control={form.control}
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,
"tag": "0014_same_hammerhead",
"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
}
]
}
+3
View File
@@ -98,6 +98,7 @@
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
"octokit": "3.1.2",
"openapi-trpc": "^0.2.0",
"otpauth": "^9.2.3",
"postgres": "3.4.4",
"public-ip": "6.0.2",
@@ -109,6 +110,7 @@
"slugify": "^1.6.6",
"sonner": "^1.4.0",
"superjson": "^2.2.1",
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"tar-fs": "3.0.5",
@@ -129,6 +131,7 @@
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/swagger-ui-react": "^4.18.3",
"@types/tar-fs": "2.0.4",
"@types/ws": "8.5.10",
"autoprefixer": "^10.4.14",
+10 -10
View File
@@ -5,14 +5,14 @@ import { createTRPCContext } from "@/server/api/trpc";
// export API handler
export default createNextApiHandler({
router: appRouter,
createContext: createTRPCContext,
onError:
process.env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
router: appRouter,
createContext: createTRPCContext,
onError:
process.env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: 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 { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
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 (
<div className="flex flex-col gap-4 w-full">
<ProfileForm />
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
</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: {},
};
}
+1273 -2
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -23,6 +23,8 @@ import { dockerRouter } from "./routers/docker";
import { composeRouter } from "./routers/compose";
import { registryRouter } from "./routers/registry";
import { clusterRouter } from "./routers/cluster";
import { generateOpenAPIDocumentFromTRPCRouter } from "openapi-trpc";
/**
* This is the primary router for your server.
*
@@ -39,6 +41,7 @@ export const appRouter = createTRPCRouter({
redis: redisRouter,
mongo: mongoRouter,
mariadb: mariadbRouter,
compose: composeRouter,
user: userRouter,
domain: domainRouter,
destination: destinationRouter,
@@ -50,10 +53,15 @@ export const appRouter = createTRPCRouter({
security: securityRouter,
redirects: redirectsRouter,
port: portRouter,
compose: composeRouter,
registry: registryRouter,
cluster: clusterRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
export const doc = generateOpenAPIDocumentFromTRPCRouter(appRouter, {
pathPrefix: "/api/trpc",
processOperation(op) {
op.security = [{ bearerAuth: [] }];
},
});
+21
View File
@@ -26,6 +26,7 @@ import {
updateAuthById,
verify2FA,
} from "../services/auth";
import { luciaToken } from "@/server/auth/token";
export const authRouter = createTRPCRouter({
createAdmin: publicProcedure
@@ -138,6 +139,23 @@ export const authRouter = createTRPCRouter({
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 }) => {
const auth = await findAuthById(input.id);
return auth;
@@ -196,4 +214,7 @@ export const authRouter = createTRPCRouter({
});
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 {
apiCreateProject,
@@ -54,6 +58,7 @@ export const projectRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
+19 -1
View File
@@ -41,6 +41,7 @@ import {
} from "../services/settings";
import { canAccessToTraefikFiles } from "../services/user";
import { recreateDirectory } from "@/server/utils/filesystem/directory";
import { doc } from "../root";
export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => {
@@ -242,5 +243,22 @@ export const settingsRouter = createTRPCRouter({
}
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
+4
View File
@@ -16,11 +16,15 @@ import { createTraefikConfig } from "@/server/utils/traefik/application";
import { docker } from "@/server/constants";
import { getAdvancedStats } from "@/server/monitoring/utilts";
import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
input: typeof apiCreateApplication._type,
) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
if (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 { cloneGitRepository } from "@/server/utils/providers/git";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Compose = typeof compose.$inferSelect;
export const createCompose = async (input: typeof apiCreateCompose._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
if (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 { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
if (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 { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
if (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 { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type MySql = typeof mysql.$inferSelect;
export const createMysql = async (input: typeof apiCreateMySql._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
if (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 { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
if (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 { eq } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Redis = typeof redis.$inferSelect;
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
export const createRedis = async (input: typeof apiCreateRedis._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
if (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 { validateRequest } from "../auth/auth";
import type { Session, User } from "lucia";
import type { OperationMeta } from "openapi-trpc";
import { validateBearerToken } from "../auth/token";
/**
* 1. CONTEXT
@@ -59,9 +61,15 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
*/
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
// const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
const { session, user } = await validateRequest(req, res);
user;
let { session, user } = await validateBearerToken(req);
if (!session) {
const cookieResult = await validateRequest(req, res);
session = cookieResult.session;
user = cookieResult.user;
}
return createInnerTRPCContext({
req,
res,
@@ -88,19 +96,22 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* errors on the backend.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
const t = initTRPC
.meta<OperationMeta>()
.context<typeof createTRPCContext>()
.create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
/**
* 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 }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
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(
req: IncomingMessage,
res: ServerResponse,
): Promise<{
user: (User & { authId: string }) | null;
session: Session | null;
}> {
): ReturnValidateToken {
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
if (!sessionId) {
return {
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(),
});
export const apiCreateApplication = createSchema
.pick({
name: true,
appName: true,
description: true,
projectId: true,
})
.transform((data) => ({
...data,
appName: `${data.appName}-${generatePassword(6)}` || generateAppName("app"),
}));
export const apiCreateApplication = createSchema.pick({
name: true,
appName: true,
description: true,
projectId: true,
});
export const apiFindOneApplication = createSchema
.pick({
+1
View File
@@ -43,6 +43,7 @@ export const auth = pgTable("auth", {
rol: roles("rol").notNull(),
image: text("image").$defaultFn(() => generateRandomImage()),
secret: text("secret"),
token: text("token"),
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
createdAt: text("createdAt")
.notNull()
+7 -13
View File
@@ -75,19 +75,13 @@ const createSchema = createInsertSchema(compose, {
composeType: z.enum(["docker-compose", "stack"]).optional(),
});
export const apiCreateCompose = createSchema
.pick({
name: true,
description: true,
projectId: true,
composeType: true,
appName: true,
})
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("compose"),
}));
export const apiCreateCompose = createSchema.pick({
name: true,
description: true,
projectId: true,
composeType: true,
appName: true,
});
export const apiCreateComposeByTemplate = createSchema
.pick({
+1 -6
View File
@@ -89,12 +89,7 @@ export const apiCreateMariaDB = createSchema
databaseUser: true,
databasePassword: true,
})
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("mariadb"),
}));
.required();
export const apiFindOneMariaDB = createSchema
.pick({
+1 -6
View File
@@ -81,12 +81,7 @@ export const apiCreateMongo = createSchema
databaseUser: true,
databasePassword: true,
})
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
}));
.required();
export const apiFindOneMongo = createSchema
.pick({
+1 -6
View File
@@ -87,12 +87,7 @@ export const apiCreateMySql = createSchema
databasePassword: true,
databaseRootPassword: true,
})
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("mysql"),
}));
.required();
export const apiFindOneMySql = createSchema
.pick({
+1 -6
View File
@@ -83,12 +83,7 @@ export const apiCreatePostgres = createSchema
projectId: true,
description: true,
})
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
}));
.required();
export const apiFindOnePostgres = createSchema
.pick({
+1 -6
View File
@@ -76,12 +76,7 @@ export const apiCreateRedis = createSchema
projectId: true,
description: true,
})
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("redis"),
}));
.required();
export const apiFindOneRedis = createSchema
.pick({
-7
View File
@@ -1,13 +1,6 @@
import { auth } from "./auth";
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", {
id: text("id").primaryKey(),
userId: text("user_id")
+2
View File
@@ -32,6 +32,7 @@ export const users = pgTable("user", {
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
@@ -105,6 +106,7 @@ export const apiAssignPermissions = createSchema
accesedServices: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,
})
.required();
+11
View File
@@ -158,3 +158,14 @@
.animate-heartbeat {
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;
}
}