wip
This commit is contained in:
parent
21cba176e9
commit
38dfd93491
8 changed files with 110 additions and 20 deletions
|
@ -16,7 +16,7 @@ FROM builder_base AS builder
|
|||
WORKDIR /app
|
||||
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
RUN pnpm fetch --ingore-scripts
|
||||
|
||||
COPY ./package*.json ./
|
||||
COPY ./prisma/schema.prisma ./prisma/
|
||||
|
|
6
src/server/common/user-permissions.ts
Normal file
6
src/server/common/user-permissions.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const USER_PERMISSIONS = {
|
||||
ADMINISTRATE_APPS: 'Can install, uninstall, start, stop and configure apps',
|
||||
ADMINISTRATE_SYSTEM: 'Can restart and update the system',
|
||||
} as const;
|
||||
|
||||
export type UserPermission = keyof typeof USER_PERMISSIONS;
|
21
src/server/migrations/00006-add-user-permissions.sql
Normal file
21
src/server/migrations/00006-add-user-permissions.sql
Normal file
|
@ -0,0 +1,21 @@
|
|||
-- Create permissions field (array of string) if it doesn't exist
|
||||
ALTER TABLE
|
||||
"user"
|
||||
ADD
|
||||
COLUMN IF NOT EXISTS "permissions" character varying [] DEFAULT NULL;
|
||||
|
||||
-- Set empty permissions array to users with null permissions
|
||||
UPDATE
|
||||
"user"
|
||||
set
|
||||
"permissions" = '{}'
|
||||
where
|
||||
"permissions" IS NULL;
|
||||
|
||||
-- Set permissions column to not null constraint
|
||||
ALTER TABLE
|
||||
"user"
|
||||
ALTER COLUMN
|
||||
"permissions"
|
||||
SET
|
||||
NOT NULL;
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
import { inferRouterOutputs } from '@trpc/server';
|
||||
import { AppServiceClass } from '../../services/apps/apps.service';
|
||||
import { router, protectedProcedure } from '../../trpc';
|
||||
import { router, protectedProcedure, permissionProcedure } from '../../trpc';
|
||||
import { prisma } from '../../db/client';
|
||||
|
||||
export type AppRouterOutput = inferRouterOutputs<typeof appRouter>;
|
||||
|
@ -10,18 +10,27 @@ const AppService = new AppServiceClass(prisma);
|
|||
const formSchema = z.object({}).catchall(z.any());
|
||||
|
||||
export const appRouter = router({
|
||||
// Protected
|
||||
getApp: protectedProcedure.input(z.object({ id: z.string() })).query(({ input }) => AppService.getApp(input.id)),
|
||||
startAllApp: protectedProcedure.mutation(AppService.startAllApps),
|
||||
startApp: protectedProcedure.input(z.object({ id: z.string() })).mutation(({ input }) => AppService.startApp(input.id)),
|
||||
installApp: protectedProcedure
|
||||
.input(z.object({ id: z.string(), form: formSchema, exposed: z.boolean().optional(), domain: z.string().optional() }))
|
||||
.mutation(({ input }) => AppService.installApp(input.id, input.form, input.exposed, input.domain)),
|
||||
updateAppConfig: protectedProcedure
|
||||
.input(z.object({ id: z.string(), form: formSchema, exposed: z.boolean().optional(), domain: z.string().optional() }))
|
||||
.mutation(({ input }) => AppService.updateAppConfig(input.id, input.form, input.exposed, input.domain)),
|
||||
stopApp: protectedProcedure.input(z.object({ id: z.string() })).mutation(({ input }) => AppService.stopApp(input.id)),
|
||||
uninstallApp: protectedProcedure.input(z.object({ id: z.string() })).mutation(({ input }) => AppService.uninstallApp(input.id)),
|
||||
updateApp: protectedProcedure.input(z.object({ id: z.string() })).mutation(({ input }) => AppService.updateApp(input.id)),
|
||||
installedApps: protectedProcedure.query(AppService.installedApps),
|
||||
listApps: protectedProcedure.query(() => AppServiceClass.listApps()),
|
||||
// Permission
|
||||
startApp: permissionProcedure('ADMINISTRATE_APPS')
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => AppService.startApp(input.id)),
|
||||
installApp: permissionProcedure('ADMINISTRATE_APPS')
|
||||
.input(z.object({ id: z.string(), form: formSchema, exposed: z.boolean().optional(), domain: z.string().optional() }))
|
||||
.mutation(({ input }) => AppService.installApp(input.id, input.form, input.exposed, input.domain)),
|
||||
updateAppConfig: permissionProcedure('ADMINISTRATE_APPS')
|
||||
.input(z.object({ id: z.string(), form: formSchema, exposed: z.boolean().optional(), domain: z.string().optional() }))
|
||||
.mutation(({ input }) => AppService.updateAppConfig(input.id, input.form, input.exposed, input.domain)),
|
||||
stopApp: permissionProcedure('ADMINISTRATE_APPS')
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => AppService.stopApp(input.id)),
|
||||
uninstallApp: permissionProcedure('ADMINISTRATE_APPS')
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => AppService.uninstallApp(input.id)),
|
||||
updateApp: permissionProcedure('ADMINISTRATE_APPS')
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => AppService.updateApp(input.id)),
|
||||
});
|
||||
|
|
|
@ -6,13 +6,15 @@ import { prisma } from '../../db/client';
|
|||
const AuthService = new AuthServiceClass(prisma);
|
||||
|
||||
export const authRouter = router({
|
||||
// Public
|
||||
login: publicProcedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async ({ input }) => AuthService.login({ ...input })),
|
||||
logout: protectedProcedure.mutation(async ({ ctx }) => AuthServiceClass.logout(ctx.session.id)),
|
||||
register: publicProcedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async ({ input }) => AuthService.register({ ...input })),
|
||||
refreshToken: protectedProcedure.mutation(async ({ ctx }) => AuthServiceClass.refreshToken(ctx.session.id)),
|
||||
me: publicProcedure.query(async ({ ctx }) => AuthService.me(ctx.session?.userId)),
|
||||
isConfigured: publicProcedure.query(async () => AuthService.isConfigured()),
|
||||
checkPasswordChangeRequest: publicProcedure.query(AuthServiceClass.checkPasswordChangeRequest),
|
||||
resetPassword: publicProcedure.input(z.object({ newPassword: z.string() })).mutation(({ input }) => AuthService.changePassword({ newPassword: input.newPassword })),
|
||||
cancelPasswordChangeRequest: publicProcedure.mutation(AuthServiceClass.cancelPasswordChangeRequest),
|
||||
// Protected
|
||||
logout: protectedProcedure.mutation(async ({ ctx }) => AuthServiceClass.logout(ctx.session.id)),
|
||||
refreshToken: protectedProcedure.mutation(async ({ ctx }) => AuthServiceClass.refreshToken(ctx.session.id)),
|
||||
});
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { inferRouterOutputs } from '@trpc/server';
|
||||
import { router, protectedProcedure, publicProcedure } from '../../trpc';
|
||||
import { router, protectedProcedure, publicProcedure, permissionProcedure } from '../../trpc';
|
||||
import { SystemServiceClass } from '../../services/system';
|
||||
|
||||
export type SystemRouterOutput = inferRouterOutputs<typeof systemRouter>;
|
||||
const SystemService = new SystemServiceClass();
|
||||
|
||||
export const systemRouter = router({
|
||||
// Public
|
||||
status: publicProcedure.query(SystemServiceClass.status),
|
||||
systemInfo: protectedProcedure.query(SystemServiceClass.systemInfo),
|
||||
getVersion: publicProcedure.query(SystemService.getVersion),
|
||||
restart: protectedProcedure.mutation(SystemService.restart),
|
||||
update: protectedProcedure.mutation(SystemService.update),
|
||||
// Protected
|
||||
systemInfo: protectedProcedure.query(SystemServiceClass.systemInfo),
|
||||
// Permission
|
||||
restart: permissionProcedure('ADMINISTRATE_SYSTEM').mutation(SystemService.restart),
|
||||
update: permissionProcedure('ADMINISTRATE_SYSTEM').mutation(SystemService.update),
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import validator from 'validator';
|
|||
import { getConfig } from '../../core/TipiConfig';
|
||||
import TipiCache from '../../core/TipiCache';
|
||||
import { fileExists, unlinkFile } from '../../common/fs.helpers';
|
||||
import { UserPermission } from '../../common/user-permissions';
|
||||
|
||||
type UsernamePasswordInput = {
|
||||
username: string;
|
||||
|
@ -94,6 +95,36 @@ export class AuthServiceClass {
|
|||
return { token };
|
||||
};
|
||||
|
||||
/*
|
||||
* Creates a new user with the provided email password and permissions.
|
||||
*
|
||||
* @param {UsernamePasswordInput} input - An object containing the email and password fields
|
||||
* @returns {Promise<User>} - A promise that resolves to the newly created user
|
||||
*/
|
||||
public createUser = async (input: UsernamePasswordInput & { permissions: UserPermission[] }) => {
|
||||
const { password, username, permissions } = input;
|
||||
const email = username.trim().toLowerCase();
|
||||
|
||||
if (!username || !password) {
|
||||
throw new Error('Missing email or password');
|
||||
}
|
||||
|
||||
if (username.length < 3 || !validator.isEmail(email)) {
|
||||
throw new Error('Invalid username');
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.findUnique({ where: { username: email } });
|
||||
|
||||
if (user) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const hash = await argon2.hash(password);
|
||||
const newUser = await this.prisma.user.create({ data: { username: email, password: hash, permissions } });
|
||||
|
||||
return newUser;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the user with the provided ID
|
||||
*
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import superjson from 'superjson';
|
||||
import { UserPermission } from './common/user-permissions';
|
||||
import { type Context } from './context';
|
||||
import { prisma } from './db/client';
|
||||
|
||||
const t = initTRPC.context<Context>().create({
|
||||
transformer: superjson,
|
||||
|
@ -13,7 +15,7 @@ export const { router } = t;
|
|||
|
||||
/**
|
||||
* Unprotected procedure
|
||||
* */
|
||||
*/
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/**
|
||||
|
@ -31,7 +33,23 @@ const isAuthed = t.middleware(({ ctx, next }) => {
|
|||
});
|
||||
});
|
||||
|
||||
const isAuthorized = (permission: UserPermission) =>
|
||||
t.middleware(async ({ ctx, next }) => {
|
||||
const user = await prisma.user.findFirst({ where: { id: Number(ctx.session?.userId) } });
|
||||
|
||||
// Operator is always authorized
|
||||
if (user?.operator) return next();
|
||||
|
||||
if (!user?.permissions.includes(permission)) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'You are not authorized to perform this action' });
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Protected procedure
|
||||
* */
|
||||
export const protectedProcedure = t.procedure.use(isAuthed);
|
||||
|
||||
export const permissionProcedure = (permission: UserPermission) => t.procedure.use(isAuthed).use(isAuthorized(permission));
|
||||
|
|
Loading…
Reference in a new issue