feat(users): add ability to disable users without deleting them
- Add disabled boolean field to user schema (PostgreSQL, MySQL, SQLite) - Block disabled users from logging in via any auth provider (credentials, LDAP, OIDC) - Check disabled status in session callback to prevent session replay - Add changeDisabled API endpoint (admin-only) - Include disabled field in getById and getAll user endpoints - Add disable/enable user toggle in admin UI with status badge in user list
This commit is contained in:
1 parent
a55dc98487
commit
4ff2c9a7fb
13 files changed
+125
-1
No files matched your search
@@ -232,6 +232,7 @@ export const userRouter = createTRPCRouter({
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
disabled: true,
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -252,6 +253,7 @@ export const userRouter = createTRPCRouter({
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
@@ -341,6 +343,7 @@ export const userRouter = createTRPCRouter({
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
provider: true,
|
||||
disabled: true,
|
||||
homeBoardId: true,
|
||||
mobileHomeBoardId: true,
|
||||
firstDayOfWeek: true,
|
||||
@@ -377,6 +380,7 @@ export const userRouter = createTRPCRouter({
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
provider: true,
|
||||
disabled: true,
|
||||
homeBoardId: true,
|
||||
mobileHomeBoardId: true,
|
||||
firstDayOfWeek: true,
|
||||
@@ -777,6 +781,23 @@ export const userRouter = createTRPCRouter({
|
||||
completedBoardTour: user?.completedBoardTour ?? false,
|
||||
};
|
||||
}),
|
||||
changeDisabled: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
disabled: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.output(z.void())
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
disabled: input.disabled,
|
||||
})
|
||||
.where(eq(users.id, input.userId));
|
||||
}),
|
||||
});
|
||||
|
||||
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
|
||||
|
||||
@@ -47,9 +47,14 @@ export const createSessionCallback = (db: Database): NextAuthCallbackOf<"session
|
||||
where: eq(users.id, user.id),
|
||||
columns: {
|
||||
colorScheme: true,
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (additionalProperties?.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...session,
|
||||
user: {
|
||||
|
||||
@@ -23,6 +23,11 @@ export const authorizeWithBasicCredentialsAsync = async (
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
logger.warn("User is disabled", { userName: user.name });
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("User is trying to log in. Checking password...", { userName: user.name });
|
||||
const isValidPassword = await comparePasswordsAsync(credentials.password, user.password);
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
provider: true,
|
||||
disabled: true,
|
||||
},
|
||||
where: and(eq(users.email, mailResult.data), eq(users.provider, "ldap")),
|
||||
});
|
||||
@@ -130,6 +131,11 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
logger.info("User created successfully", { userName: credentials.name });
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
logger.warn("User is disabled", { userName: credentials.name });
|
||||
throw new CredentialsSignin("User is disabled");
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: credentials.name,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `user` ADD `disabled` boolean DEFAULT false NOT NULL;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "disabled" boolean DEFAULT false NOT NULL;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "disabled" integer DEFAULT 0 NOT NULL;
|
||||
@@ -72,6 +72,7 @@ export const users = mysqlTable("user", {
|
||||
image: text(),
|
||||
password: text(),
|
||||
provider: varchar({ length: 64 }).$type<SupportedAuthProvider>().default("credentials").notNull(),
|
||||
disabled: boolean().default(false).notNull(),
|
||||
homeBoardId: varchar({ length: 64 }).references((): AnyMySqlColumn => boards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
@@ -71,6 +71,7 @@ export const users = pgTable("user", {
|
||||
image: text(),
|
||||
password: text(),
|
||||
provider: varchar({ length: 64 }).$type<SupportedAuthProvider>().default("credentials").notNull(),
|
||||
disabled: boolean().default(false).notNull(),
|
||||
homeBoardId: varchar({ length: 64 }).references((): AnyPgColumn => boards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
@@ -54,6 +54,7 @@ export const users = sqliteTable("user", {
|
||||
image: text(),
|
||||
password: text(),
|
||||
provider: text().$type<SupportedAuthProvider>().default("credentials").notNull(),
|
||||
disabled: int({ mode: "boolean" }).default(false).notNull(),
|
||||
homeBoardId: text().references((): AnySQLiteColumn => boards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
Reference in new issue
Block a user