Skip to content

Commit

Permalink
Implements basic user locking for admins
Browse files Browse the repository at this point in the history
  • Loading branch information
zomars committed Nov 17, 2023
1 parent 57a6540 commit 8eb024a
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 42 deletions.
1 change: 1 addition & 0 deletions packages/features/auth/lib/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export enum ErrorCode {
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
RateLimitExceeded = "rate-limit-exceeded",
SocialIdentityProviderRequired = "social-identity-provider-required",
UserAccountLocked = "user-account-locked",
}
5 changes: 5 additions & 0 deletions packages/features/auth/lib/next-auth-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ const providers: Provider[] = [
throw new Error(ErrorCode.IncorrectEmailPassword);
}

// Locked users cannot login
if (user.locked) {
throw new Error(ErrorCode.UserAccountLocked);
}

await checkRateLimitAndThrowError({
identifier: user.email,
});
Expand Down
37 changes: 37 additions & 0 deletions packages/features/ee/users/components/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ function UsersTableBare() {
},
});

const lockUserAccount = trpc.viewer.admin.lockUserAccount.useMutation({
onSuccess: ({ userId, locked }) => {
showToast(locked ? "User was locked" : "User was unlocked", "success");
utils.viewer.admin.listPaginated.setInfiniteData({ limit: FETCH_LIMIT }, (cachedData) => {
if (!cachedData) {
return {
pages: [],
pageParams: [],
};
}
return {
...cachedData,
pages: cachedData.pages.map((page) => ({
...page,
rows: page.rows.map((row) => {
const newUser = row;
console.log("newUser", newUser);
if (row.id === userId) newUser.locked = locked;
return newUser;
}),
})),
};
});
},
});

//we must flatten the array of arrays from the useInfiniteQuery hook
const flatData = useMemo(() => data?.pages?.flatMap((page) => page.rows) ?? [], [data]);
const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
Expand Down Expand Up @@ -140,6 +166,11 @@ function UsersTableBare() {
<div className="text-subtle ml-4 font-medium">
<span className="text-default">{user.name}</span>
<span className="ml-3">/{user.username}</span>
{user.locked && (
<span className="ml-3">
<Lock />
</span>
)}
<br />
<span className="break-all">{user.email}</span>
</div>
Expand Down Expand Up @@ -167,6 +198,12 @@ function UsersTableBare() {
onClick: () => sendPasswordResetEmail.mutate({ userId: user.id }),
icon: Lock,
},
{
id: "lock-user",
label: user.locked ? "Unlock User Account" : "Lock User Account",
onClick: () => lockUserAccount.mutate({ userId: user.id, locked: !user.locked }),
icon: Lock,
},
{
id: "delete",
label: "Delete",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "locked" BOOLEAN NOT NULL DEFAULT false;
3 changes: 3 additions & 0 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ model User {
//linkedBy User? @relation("linked_account", fields: [linkedByUserId], references: [id], onDelete: Cascade)
//linkedUsers User[] @relation("linked_account")*/
// Used to lock the user account
locked Boolean @default(false)
@@unique([email])
@@unique([email, username])
@@unique([username, organizationId])
Expand Down
2 changes: 2 additions & 0 deletions packages/trpc/server/middlewares/sessionMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
// Locked users can't login
locked: false,
},
select: {
id: true,
Expand Down
62 changes: 21 additions & 41 deletions packages/trpc/server/routers/viewer/admin/_router.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,34 @@
import { z } from "zod";

import { authedAdminProcedure } from "../../../procedures/authedProcedure";
import { router } from "../../../trpc";
import { router, importHandler } from "../../../trpc";
import { ZListMembersSchema } from "./listPaginated.schema";
import { ZAdminLockUserAccountSchema } from "./lockUserAccount.schema";
import { ZAdminPasswordResetSchema } from "./sendPasswordReset.schema";

type AdminRouterHandlerCache = {
listPaginated?: typeof import("./listPaginated.handler").listPaginatedHandler;
sendPasswordReset?: typeof import("./sendPasswordReset.handler").sendPasswordResetHandler;
};
const NAMESPACE = "admin";

const UNSTABLE_HANDLER_CACHE: AdminRouterHandlerCache = {};
const namespaced = (s: string) => `${NAMESPACE}.${s}`;

export const adminRouter = router({
listPaginated: authedAdminProcedure.input(ZListMembersSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.listPaginated) {
UNSTABLE_HANDLER_CACHE.listPaginated = await import("./listPaginated.handler").then(
(mod) => mod.listPaginatedHandler
);
}

// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.listPaginated) {
throw new Error("Failed to load handler");
}

return UNSTABLE_HANDLER_CACHE.listPaginated({
ctx,
input,
});
listPaginated: authedAdminProcedure.input(ZListMembersSchema).query(async (opts) => {
const handler = await importHandler(namespaced("listPaginated"), () => import("./listPaginated.handler"));
return handler(opts);
}),
sendPasswordReset: authedAdminProcedure.input(ZAdminPasswordResetSchema).mutation(async (opts) => {
const handler = await importHandler(
namespaced("sendPasswordReset"),
() => import("./sendPasswordReset.handler")
);
return handler(opts);
}),
lockUserAccount: authedAdminProcedure.input(ZAdminLockUserAccountSchema).mutation(async (opts) => {
const handler = await importHandler(
namespaced("lockUserAccount"),
() => import("./lockUserAccount.handler")
);
return handler(opts);
}),
sendPasswordReset: authedAdminProcedure
.input(ZAdminPasswordResetSchema)
.mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.sendPasswordReset) {
UNSTABLE_HANDLER_CACHE.sendPasswordReset = await import("./sendPasswordReset.handler").then(
(mod) => mod.sendPasswordResetHandler
);
}

// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.sendPasswordReset) {
throw new Error("Failed to load handler");
}

return UNSTABLE_HANDLER_CACHE.sendPasswordReset({
ctx,
input,
});
}),
toggleFeatureFlag: authedAdminProcedure
.input(z.object({ slug: z.string(), enabled: z.boolean() }))
.mutation(({ ctx, input }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type GetOptions = {
input: TListMembersSchema;
};

export const listPaginatedHandler = async ({ input }: GetOptions) => {
const listPaginatedHandler = async ({ input }: GetOptions) => {
const { cursor, limit, searchTerm } = input;

const getTotalUsers = await prisma.user.count();
Expand Down Expand Up @@ -67,3 +67,5 @@ export const listPaginatedHandler = async ({ input }: GetOptions) => {
},
};
};

export default listPaginatedHandler;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { prisma } from "@calcom/prisma";

import type { TrpcSessionUser } from "../../../trpc";
import type { TAdminLockUserAccountSchema } from "./lockUserAccount.schema";

type GetOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TAdminLockUserAccountSchema;
};

const lockUserAccountHandler = async ({ input }: GetOptions) => {
const { userId, locked } = input;

const user = await prisma.user.update({
where: {
id: userId,
},
data: {
locked,
},
});

if (!user) {
throw new Error("User not found");
}

return {
success: true,
userId,
locked,
};
};

export default lockUserAccountHandler;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from "zod";

export const ZAdminLockUserAccountSchema = z.object({
userId: z.number(),
locked: z.boolean(),
});

export type TAdminLockUserAccountSchema = z.infer<typeof ZAdminLockUserAccountSchema>;

0 comments on commit 8eb024a

Please sign in to comment.