feat: wip cli token authentication

This commit is contained in:
Mauricio Siu 2024-06-05 22:42:11 -06:00
parent 113df9ae12
commit b9bff95c3d
16 changed files with 2845 additions and 56 deletions

31
COMMANDS Normal file
View File

@ -0,0 +1,31 @@
### Jerarquía de Comandos
1. **Comandos de Proyecto**:
* Crear, listar, eliminar y gestionar proyectos.
2. **Comandos de Aplicaciones**:
* Crear, listar, eliminar y gestionar aplicaciones dentro de un proyecto.
3. **Comandos de Bases de Datos**:
* Crear, listar, eliminar y gestionar bases de datos dentro de un proyecto.
### Estructura de Comandos
#### Proyecto
* `dokploy project create`: Crear un nuevo proyecto.
* `dokploy project list`: Listar todos los proyectos.
* `dokploy project delete`: Eliminar un proyecto.
* `dokploy project info`: Mostrar información detallada de un proyecto.
#### Aplicaciones
* `dokploy app create`: Crear una nueva aplicación dentro de un proyecto.
* `dokploy app list`: Listar todas las aplicaciones dentro de un proyecto.
* `dokploy app delete`: Eliminar una aplicación dentro de un proyecto.
* `dokploy app info`: Mostrar información detallada de una aplicación.
#### Bases de Datos
* `dokploy db create`: Crear una nueva base de datos dentro de un proyecto.
* `dokploy db list`: Listar todas las bases de datos dentro de un proyecto.
* `dokploy db delete`: Eliminar una base de datos dentro de un proyecto.
* `dokploy db info`: Mostrar información detallada de una base de datos.

View File

@ -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 || "",
@ -182,6 +185,18 @@ export const ProfileForm = () => {
</div>
</form>
</Form>
<div className="flex flex-row gap-4 pt-10">
<Input placeholder="Token" disabled value={data?.token || ""} />
<Button
type="button"
isLoading={isLoadingToken}
onClick={async () => {
await generateToken();
}}
>
Save
</Button>
</div>
</CardContent>
</Card>
);

18
docker-compose.yml Normal file
View File

@ -0,0 +1,18 @@
version: '3.8'
services:
app:
image: nginx:latest
labels:
- 'traefik.http.routers.nginx.rule=Host(`test.docker.localhost`)'
- 'traefik.http.routers.nginx.entrypoints=web'
- 'traefik.http.services.nginx.loadbalancer.server.port=80'
networks:
- dokploy-network
networks:
dokploy-network:
external: true

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -106,6 +106,13 @@
"when": 1716715367982,
"tag": "0014_same_hammerhead",
"breakpoints": true
},
{
"idx": 15,
"version": "6",
"when": 1717564517104,
"tag": "0015_fearless_callisto",
"breakpoints": true
}
]
}

View File

@ -57,6 +57,7 @@ export const applicationRouter = createTRPCRouter({
.input(apiCreateApplication)
.mutation(async ({ input, ctx }) => {
try {
console.log(input);
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
@ -65,6 +66,8 @@ export const applicationRouter = createTRPCRouter({
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newApplication.applicationId);
}
return newApplication;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",

View File

@ -26,6 +26,7 @@ import {
updateAuthById,
verify2FA,
} from "../services/auth";
import { TimeSpan } from "lucia";
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 lucia.invalidateSession(auth.token);
}
const session = await lucia.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;
}),
});

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,
@ -44,6 +48,30 @@ export const projectRouter = createTRPCRouter({
await addNewProject(ctx.user.authId, project.projectId);
}
return project;
} catch (error) {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the project",
cause: error,
});
}
}),
createCLI: protectedProcedure
.input(apiCreateProject)
.mutation(async ({ ctx, input }) => {
try {
console.log(ctx);
if (ctx.user.rol === "user") {
await checkProjectAccess(ctx.user.authId, "create");
}
const project = await createProject(input);
if (ctx.user.rol === "user") {
await addNewProject(ctx.user.authId, project.projectId);
}
return project;
} catch (error) {
console.log(error);

View File

@ -13,7 +13,7 @@ import { TRPCError, initTRPC } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import { ZodError } from "zod";
import { validateRequest } from "../auth/auth";
import { validateBearerToken, validateRequest } from "../auth/auth";
import type { Session, User } from "lucia";
/**
@ -59,9 +59,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,
@ -147,6 +153,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" });

View File

@ -16,7 +16,7 @@ export const lucia = new Lucia(adapter, {
secure: false,
},
},
sessionExpiresIn: new TimeSpan(1, "d"),
// sessionExpiresIn: new TimeSpan(1, "d"),
getUserAttributes: (attributes) => {
return {
email: attributes.email,
@ -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,
@ -90,3 +93,32 @@ export async function validateWebSocketRequest(
const result = await lucia.validateSession(sessionId);
return result;
}
export const validateBearerToken = async (
req: IncomingMessage,
): ReturnValidateToken => {
const authorizationHeader = req.headers.authorization;
const sessionId = lucia.readBearerToken(authorizationHeader ?? "");
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await lucia.validateSession(sessionId);
return {
session: result.session,
...((result.user && {
user: {
authId: result.user.id,
email: result.user.email,
rol: result.user.rol,
id: result.user.id,
secret: result.user.secret,
},
}) || {
user: null,
}),
};
};

View File

@ -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()

View File

@ -53,6 +53,11 @@ export const apiCreateProject = createSchema.pick({
description: true,
});
export const apiCreateCLI = createSchema.pick({
name: true,
description: true,
});
export const apiFindOneProject = createSchema
.pick({
projectId: true,

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")

View File

@ -142,6 +142,7 @@ export const startService = async (appName: string) => {
try {
await execAsync(`docker service scale ${appName}=1 `);
} catch (error) {
// Cambiar esto para que no erroje un error
throw error;
}
};