mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-11 05:59:23 -06:00
feat: enhance user session model with role information and update authentication utils
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -44,7 +44,6 @@ export default async function S3TargetsPage() {
|
||||
<RolesTable roles={roles} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../../prisma/null"
|
||||
|
||||
|
||||
export const ParameterModel = z.object({
|
||||
name: z.string(),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../../prisma/null"
|
||||
|
||||
|
||||
export const VerificationTokenModel = z.object({
|
||||
identifier: z.string(),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -2,4 +2,6 @@ import { Session } from "next-auth";
|
||||
|
||||
export interface UserSession {
|
||||
email: string;
|
||||
roleName?: string;
|
||||
roleId?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user