feat: enhance user session model with role information and update authentication utils

This commit is contained in:
biersoeckli
2025-03-06 07:16:55 +00:00
parent 21decda0fe
commit 9ae6168de4
24 changed files with 151 additions and 22 deletions

View File

@@ -9,6 +9,7 @@ import userService from "@/server/services/user.service";
import { saveFormAction } from "@/server/utils/action-wrapper.utils";
import ipAddressFinderAdapter from "@/server/adapter/ip-adress-finder.adapter";
import traefikMeDomainStandaloneService from "@/server/services/standalone-services/traefik-me-domain-standalone.service";
import roleService from "@/server/services/role.service";
export const registerUser = async (prevState: any, inputData: RegisterFormInputSchema) =>
@@ -17,7 +18,8 @@ export const registerUser = async (prevState: any, inputData: RegisterFormInputS
if (allUsers.length !== 0) {
throw new ServiceException("User registration is currently not possible");
}
await userService.registerUser(validatedData.email, validatedData.password);
const adminRole = await roleService.getOrCreateAdminRole();
await userService.registerUser(validatedData.email, validatedData.password, adminRole.id);
await quickStackService.createOrUpdateCertIssuer(validatedData.email);
try {

View File

@@ -43,7 +43,6 @@ export default function BasicAuthEditDialog({
const [isOpen, setIsOpen] = useState<boolean>(false);
const form = useForm<BasicAuthEditModel>({
resolver: zodResolver(basicAuthEditZodModel.merge(z.object({
appId: z.string().nullish()

View File

@@ -44,7 +44,6 @@ export default async function S3TargetsPage() {
<RolesTable roles={roles} />
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -2,13 +2,41 @@ import { Prisma } from "@prisma/client";
import dataAccess from "../adapter/db.client";
import { revalidateTag, unstable_cache } from "next/cache";
import { Tags } from "../utils/cache-tag-generator.utils";
import { ServiceException } from "@/shared/model/service.exception.model";
const adminRoleName = 'Admin';
export const adminRoleName = "admin";
export enum RolePersmission {
READ = "READ",
READWRITE = "READWRITE",
}
export class RoleService {
async getRoleByUserId(userId: string) {
return await unstable_cache(async (uId: string) => await dataAccess.client.role.findFirst({
select: {
id: true,
name: true,
},
where: {
users: {
some: {
id: uId
}
}
}
}),
[Tags.roles(), Tags.users()], {
tags: [Tags.roles(), Tags.users()]
})(userId);
}
async save(item: Prisma.RoleUncheckedCreateInput | Prisma.RoleUncheckedUpdateInput) {
try {
if (item.name === adminRoleName) {
throw new ServiceException("You cannot assign the name 'admin' to a role");
}
if (item.id) {
await dataAccess.client.role.update({
where: {
@@ -100,6 +128,22 @@ export class RoleService {
revalidateTag(Tags.users());
}
}
async getOrCreateAdminRole() {
let adminRole = await dataAccess.client.role.findFirst({
where: {
name: adminRoleName
}
});
if (!adminRole) {
adminRole = await dataAccess.client.role.create({
data: {
name: adminRoleName
}
});
}
return adminRole;
}
}
const roleService = new RoleService();

View File

@@ -7,6 +7,7 @@ import { ServerActionResult } from "@/shared/model/server-action-error-return.mo
import { FormValidationException } from "@/shared/model/form-validation-exception.model";
import { authOptions } from "@/server/utils/auth-options";
import { NextResponse } from "next/server";
import roleService, { adminRoleName, RolePersmission } from "../services/role.service";
/**
* THIS FUNCTION RETURNS NULL IF NO USER IS LOGGED IN
@@ -18,7 +19,9 @@ export async function getUserSession(): Promise<UserSession | null> {
return null;
}
return {
email: session?.user?.email as string
email: session?.user?.email as string,
roleId: (session?.user as any)?.roleId as string,
roleName: (session?.user as any)?.roleName as string
};
}
@@ -31,6 +34,71 @@ export async function getAuthUserSession(): Promise<UserSession> {
return session;
}
export async function getAdminUserSession(): Promise<UserSession> {
const session = await getAuthUserSession();
if (!isAdmin(session)) {
console.error('User is not admin.');
throw new ServiceException('User is not authorized for this action.');
}
return session;
}
function isAdmin(session: UserSession) {
return session.roleName === adminRoleName;
}
export async function isAuthorizedForRoleId(roleId: string) {
const session = await getAuthUserSession();
if (!isAdmin(session) && session.roleId !== roleId) {
console.error('User is not authorized for role: ' + roleId);
throw new ServiceException('User is not authorized for this action.');
}
return session;
}
export async function isAuthorizedForRoleName(roleName: string) {
const session = await getAuthUserSession();
if (!isAdmin(session) && session.roleName !== roleName) {
console.error('User is not authorized for role: ' + roleName);
throw new ServiceException('User is not authorized for this action.');
}
return session;
}
export async function isAuthorizedReadForApp(appId: string) {
const session = await getAuthUserSession();
if (isAdmin(session)) {
return session;
}
if (!session.roleId) {
console.error('User is not authorized for app: ' + appId);
throw new ServiceException('User is not authorized for this action.');
}
const role = await roleService.getById(session.roleId);
if (!isAdmin(session) && !role.roleAppPermissions.some(app => app.appId === appId && app.permission === RolePersmission.READ)) {
console.error('User is not authorized for app: ' + appId);
throw new ServiceException('User is not authorized for this action.');
}
return session;
}
export async function isAuthorizedWriteForApp(appId: string) {
const session = await getAuthUserSession();
if (isAdmin(session)) {
return session;
}
if (!session.roleId) {
console.error('User is not authorized for app: ' + appId);
throw new ServiceException('User is not authorized for this action.');
}
const role = await roleService.getById(session.roleId);
if (!isAdmin(session) && !role.roleAppPermissions.some(app => app.appId === appId && app.permission === RolePersmission.READWRITE)) {
console.error('User is not authorized for app: ' + appId);
throw new ServiceException('User is not authorized for this action.');
}
return session;
}
export async function saveFormAction<ReturnType, TInputData, ZodType extends ZodRawShape>(
inputData: TInputData,
validationModel: ZodObject<ZodType>,

View File

@@ -8,6 +8,7 @@ import dataAccess from "@/server/adapter/db.client";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcrypt";
import userService from "@/server/services/user.service";
import roleService from "@/server/services/role.service";
const saltRounds = 10;
@@ -54,6 +55,20 @@ export const authOptions: NextAuthOptions = {
}
})
],
callbacks: {
async jwt({ token, user }) {
// Initial sign in
return token;
},
async session({ session, token, user }) {
if (token?.sub) {
const role = await roleService.getRoleByUserId(token.sub);
(session.user as any).roleName = role?.name;
(session.user as any).roleId = role?.id;
}
return session;
},
},
adapter: PrismaAdapter(dataAccess.client),
};

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteUser, RelatedUserModel } from "./index"
export const AccountModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteProject, RelatedProjectModel, CompleteAppDomain, RelatedAppDomainModel, CompleteAppPort, RelatedAppPortModel, CompleteAppVolume, RelatedAppVolumeModel, CompleteAppFileMount, RelatedAppFileMountModel, CompleteAppBasicAuth, RelatedAppBasicAuthModel, CompleteRoleAppPermission, RelatedRoleAppPermissionModel } from "./index"
export const AppModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteApp, RelatedAppModel } from "./index"
export const AppBasicAuthModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteApp, RelatedAppModel } from "./index"
export const AppDomainModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteApp, RelatedAppModel } from "./index"
export const AppFileMountModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteApp, RelatedAppModel } from "./index"
export const AppPortModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteApp, RelatedAppModel, CompleteVolumeBackup, RelatedVolumeBackupModel } from "./index"
export const AppVolumeModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteUser, RelatedUserModel } from "./index"
export const AuthenticatorModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
export const ParameterModel = z.object({
name: z.string(),

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteApp, RelatedAppModel } from "./index"
export const ProjectModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteUser, RelatedUserModel, CompleteRoleAppPermission, RelatedRoleAppPermissionModel } from "./index"
export const RoleModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteRole, RelatedRoleModel, CompleteApp, RelatedAppModel } from "./index"
export const RoleAppPermissionModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteVolumeBackup, RelatedVolumeBackupModel } from "./index"
export const S3TargetModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteUser, RelatedUserModel } from "./index"
export const SessionModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteRole, RelatedRoleModel, CompleteAccount, RelatedAccountModel, CompleteSession, RelatedSessionModel, CompleteAuthenticator, RelatedAuthenticatorModel } from "./index"
export const UserModel = z.object({

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
export const VerificationTokenModel = z.object({
identifier: z.string(),

View File

@@ -1,5 +1,5 @@
import * as z from "zod"
import * as imports from "../../../../prisma/null"
import { CompleteAppVolume, RelatedAppVolumeModel, CompleteS3Target, RelatedS3TargetModel } from "./index"
export const VolumeBackupModel = z.object({

View File

@@ -2,4 +2,6 @@ import { Session } from "next-auth";
export interface UserSession {
email: string;
roleName?: string;
roleId?: string;
}