chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import {
|
||||
PermissionOverrides,
|
||||
SystemRole,
|
||||
resolvePermissions,
|
||||
type ColumnPreferences,
|
||||
} from "@planarchy/shared/types";
|
||||
import {
|
||||
dashboardLayoutSchema,
|
||||
normalizeDashboardLayout,
|
||||
} from "@planarchy/shared/schemas";
|
||||
import { Prisma } from "@planarchy/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc.js";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
list: protectedProcedure.query(async ({ ctx }) => {
|
||||
return ctx.db.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
systemRole: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
}),
|
||||
|
||||
me: protectedProcedure.query(async ({ ctx }) => {
|
||||
const user = await ctx.db.user.findUnique({
|
||||
where: { email: ctx.session.user?.email ?? "" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
systemRole: true,
|
||||
permissionOverrides: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||
}
|
||||
|
||||
return user;
|
||||
}),
|
||||
|
||||
create: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
systemRole: z.nativeEnum(SystemRole).default(SystemRole.USER),
|
||||
password: z.string().min(8),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const existing = await ctx.db.user.findUnique({ where: { email: input.email } });
|
||||
if (existing) {
|
||||
throw new TRPCError({ code: "CONFLICT", message: "User with this email already exists" });
|
||||
}
|
||||
|
||||
const { hash } = await import("@node-rs/argon2");
|
||||
const passwordHash = await hash(input.password);
|
||||
|
||||
return ctx.db.user.create({
|
||||
data: {
|
||||
email: input.email,
|
||||
name: input.name,
|
||||
systemRole: input.systemRole,
|
||||
passwordHash,
|
||||
},
|
||||
select: { id: true, name: true, email: true, systemRole: true },
|
||||
});
|
||||
}),
|
||||
|
||||
updateRole: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
systemRole: z.nativeEnum(SystemRole),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return ctx.db.user.update({
|
||||
where: { id: input.id },
|
||||
data: { systemRole: input.systemRole },
|
||||
select: { id: true, name: true, email: true, systemRole: true },
|
||||
});
|
||||
}),
|
||||
|
||||
getDashboardLayout: protectedProcedure.query(async ({ ctx }) => {
|
||||
const user = await ctx.db.user.findUnique({
|
||||
where: { email: ctx.session.user?.email ?? "" },
|
||||
select: { dashboardLayout: true, updatedAt: true },
|
||||
});
|
||||
return {
|
||||
layout: user?.dashboardLayout ? normalizeDashboardLayout(user.dashboardLayout) : null,
|
||||
updatedAt: user?.updatedAt ?? null,
|
||||
};
|
||||
}),
|
||||
|
||||
saveDashboardLayout: protectedProcedure
|
||||
.input(z.object({ layout: dashboardLayoutSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const updated = await ctx.db.user.update({
|
||||
where: { email: ctx.session.user?.email ?? "" },
|
||||
data: { dashboardLayout: input.layout as unknown as import("@planarchy/db").Prisma.InputJsonValue },
|
||||
select: { updatedAt: true },
|
||||
});
|
||||
return { updatedAt: updated.updatedAt };
|
||||
}),
|
||||
|
||||
setPermissions: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
overrides: z
|
||||
.object({
|
||||
granted: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional(),
|
||||
chapterIds: z.array(z.string()).optional(),
|
||||
})
|
||||
.nullable(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
data: { permissionOverrides: input.overrides ?? Prisma.DbNull },
|
||||
});
|
||||
return user;
|
||||
}),
|
||||
|
||||
resetPermissions: adminProcedure
|
||||
.input(z.object({ userId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
data: { permissionOverrides: Prisma.DbNull },
|
||||
});
|
||||
}),
|
||||
|
||||
getColumnPreferences: protectedProcedure.query(async ({ ctx }) => {
|
||||
const user = await ctx.db.user.findUnique({
|
||||
where: { id: ctx.dbUser!.id },
|
||||
select: { columnPreferences: true },
|
||||
});
|
||||
return (user?.columnPreferences ?? {}) as ColumnPreferences;
|
||||
}),
|
||||
|
||||
setColumnPreferences: protectedProcedure
|
||||
.input(z.object({
|
||||
view: z.enum(["resources", "projects", "allocations", "vacations", "roles", "users", "blueprints"]),
|
||||
visible: z.array(z.string()).optional(),
|
||||
sort: z.object({ field: z.string(), dir: z.enum(["asc", "desc"]) }).nullable().optional(),
|
||||
rowOrder: z.array(z.string()).nullable().optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const existing = await ctx.db.user.findUnique({
|
||||
where: { id: ctx.dbUser!.id },
|
||||
select: { columnPreferences: true },
|
||||
});
|
||||
const prefs = (existing?.columnPreferences ?? {}) as ColumnPreferences;
|
||||
const prev = (prefs[input.view] as import("@planarchy/shared").ViewPreferences | undefined) ?? { visible: [] };
|
||||
|
||||
// Merge: only overwrite fields that were explicitly provided
|
||||
const merged: import("@planarchy/shared").ViewPreferences = {
|
||||
visible: input.visible ?? prev.visible,
|
||||
};
|
||||
// sort: null = clear, undefined = keep existing, value = set
|
||||
if (input.sort !== null && input.sort !== undefined) {
|
||||
merged.sort = input.sort;
|
||||
} else if (input.sort === undefined && prev.sort != null) {
|
||||
merged.sort = prev.sort;
|
||||
}
|
||||
// rowOrder: null = clear, undefined = keep existing, value = set
|
||||
if (input.rowOrder !== null && input.rowOrder !== undefined) {
|
||||
merged.rowOrder = input.rowOrder;
|
||||
} else if (input.rowOrder === undefined && prev.rowOrder != null) {
|
||||
merged.rowOrder = prev.rowOrder;
|
||||
}
|
||||
|
||||
prefs[input.view] = merged;
|
||||
await ctx.db.user.update({
|
||||
where: { id: ctx.dbUser!.id },
|
||||
data: { columnPreferences: prefs as Prisma.InputJsonValue },
|
||||
});
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
getEffectivePermissions: adminProcedure
|
||||
.input(z.object({ userId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const user = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.userId },
|
||||
select: { systemRole: true, permissionOverrides: true },
|
||||
});
|
||||
const permissions = resolvePermissions(
|
||||
user.systemRole as SystemRole,
|
||||
user.permissionOverrides as PermissionOverrides | null,
|
||||
);
|
||||
return {
|
||||
systemRole: user.systemRole,
|
||||
effectivePermissions: Array.from(permissions),
|
||||
overrides: user.permissionOverrides as PermissionOverrides | null,
|
||||
};
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user