Skip to content

Commit ec49702

Browse files
authored
Merge pull request #6 from database-playground/pan93412/dbp-14-allow-logout-all-devices-of-a-user-in-admin-panel
2 parents ea331af + 52e96d2 commit ec49702

File tree

7 files changed

+156
-4
lines changed

7 files changed

+156
-4
lines changed

app/(admin)/(user-management)/users/[id]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SiteHeader } from "@/components/site-header";
22
import { DeleteUserButtonTrigger } from "../_components/delete";
3+
import { LogoutUserDevicesButtonTrigger } from "../_components/logout-devices";
34
import { UpdateUserButtonTrigger } from "../_components/update";
45
import { AuditInfoCard } from "./_components/audit-info";
56
import { GroupsCard } from "./_components/groups";
@@ -25,6 +26,7 @@ export default async function UserPage({
2526
<Header id={id as string} />
2627

2728
<div className="flex items-center gap-2">
29+
<LogoutUserDevicesButtonTrigger id={id as string} />
2830
<UpdateUserButtonTrigger id={id as string} />
2931
<DeleteUserButtonTrigger id={id as string} />
3032
</div>

app/(admin)/(user-management)/users/_components/data-table-columns.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { ColumnDef } from "@tanstack/react-table";
1313
import { MoreHorizontal } from "lucide-react";
1414
import Link from "next/link";
1515
import { DeleteUserDropdownTrigger } from "./delete";
16+
import { LogoutUserDevicesDropdownTrigger } from "./logout-devices";
1617
import { UpdateUserDropdownTrigger } from "./update";
1718

1819
export interface User {
@@ -104,6 +105,8 @@ export const columns: ColumnDef<User>[] = [
104105
<DropdownMenuSeparator />
105106
<UpdateUserDropdownTrigger id={row.original.id} />
106107
<DeleteUserDropdownTrigger id={row.original.id} />
108+
<DropdownMenuSeparator />
109+
<LogoutUserDevicesDropdownTrigger id={row.original.id} />
107110
</DropdownMenuContent>
108111
</DropdownMenu>
109112
);
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"use client";
2+
3+
import {
4+
AlertDialog,
5+
AlertDialogAction,
6+
AlertDialogCancel,
7+
AlertDialogContent,
8+
AlertDialogDescription,
9+
AlertDialogFooter,
10+
AlertDialogHeader,
11+
AlertDialogTitle,
12+
AlertDialogTrigger,
13+
} from "@/components/ui/alert-dialog";
14+
import { buttonVariants } from "@/components/ui/button";
15+
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
16+
import { useMutation, useSuspenseQuery } from "@apollo/client/react";
17+
import { LogOut } from "lucide-react";
18+
import { useRouter } from "next/navigation";
19+
import { Suspense, useState } from "react";
20+
import { toast } from "sonner";
21+
import { USER_LOGOUT_DEVICES_MUTATION } from "./mutation";
22+
import { USER_BY_ID_QUERY } from "./query";
23+
24+
export function LogoutUserDevicesDropdownTrigger({ id }: { id: string }) {
25+
const router = useRouter();
26+
const [open, setOpen] = useState(false);
27+
28+
return (
29+
<AlertDialog open={open} onOpenChange={setOpen}>
30+
<AlertDialogTrigger asChild>
31+
<DropdownMenuItem
32+
onClick={(e) => {
33+
e.preventDefault();
34+
setOpen(true);
35+
}}
36+
>
37+
登出所有裝置
38+
</DropdownMenuItem>
39+
</AlertDialogTrigger>
40+
41+
<Suspense>
42+
<LogoutUserDevicesAlertDialogContent
43+
id={id}
44+
onCompleted={() => {
45+
setOpen(false);
46+
router.refresh();
47+
}}
48+
/>
49+
</Suspense>
50+
</AlertDialog>
51+
);
52+
}
53+
54+
export function LogoutUserDevicesButtonTrigger({ id }: { id: string }) {
55+
const [open, setOpen] = useState(false);
56+
const router = useRouter();
57+
58+
return (
59+
<AlertDialog open={open} onOpenChange={setOpen}>
60+
<AlertDialogTrigger className={buttonVariants({ variant: "outline" })}>
61+
<LogOut className="h-4 w-4" />
62+
<span>登出所有裝置</span>
63+
</AlertDialogTrigger>
64+
65+
<Suspense>
66+
<LogoutUserDevicesAlertDialogContent
67+
id={id}
68+
onCompleted={() => {
69+
setOpen(false);
70+
router.refresh();
71+
}}
72+
/>
73+
</Suspense>
74+
</AlertDialog>
75+
);
76+
}
77+
78+
function LogoutUserDevicesAlertDialogContent({
79+
id,
80+
onCompleted,
81+
}: {
82+
id: string;
83+
onCompleted: () => void;
84+
}) {
85+
const { data } = useSuspenseQuery(USER_BY_ID_QUERY, {
86+
variables: { id },
87+
});
88+
89+
const [logoutUserDevices] = useMutation(USER_LOGOUT_DEVICES_MUTATION, {
90+
onError(error) {
91+
toast.error("登出所有裝置失敗", {
92+
description: error.message,
93+
});
94+
},
95+
96+
onCompleted() {
97+
toast.success("已成功登出使用者的所有裝置");
98+
onCompleted();
99+
},
100+
});
101+
102+
return (
103+
<AlertDialogContent>
104+
<AlertDialogHeader>
105+
<AlertDialogTitle>
106+
確定要登出「{data.user.name}」的所有裝置嗎?
107+
</AlertDialogTitle>
108+
<AlertDialogDescription>
109+
此操作會強制登出該使用者在所有裝置上的登入狀態,使用者需要重新登入才能繼續使用服務。
110+
</AlertDialogDescription>
111+
</AlertDialogHeader>
112+
<AlertDialogFooter>
113+
<AlertDialogCancel>取消</AlertDialogCancel>
114+
<AlertDialogAction
115+
onClick={() => logoutUserDevices({ variables: { userID: id } })}
116+
>
117+
登出所有裝置
118+
</AlertDialogAction>
119+
</AlertDialogFooter>
120+
</AlertDialogContent>
121+
);
122+
}

app/(admin)/(user-management)/users/_components/mutation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ export const USER_DELETE_MUTATION = graphql(`
1313
deleteUser(id: $id)
1414
}
1515
`);
16+
17+
export const USER_LOGOUT_DEVICES_MUTATION = graphql(`
18+
mutation LogoutUserDevices($userID: ID!) {
19+
logoutUser(userID: $userID)
20+
}
21+
`);

gql/gql.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type Documents = {
5151
"\n query UserAuditInfo($id: ID!) {\n user(id: $id) {\n id\n createdAt\n updatedAt\n }\n }\n": typeof types.UserAuditInfoDocument,
5252
"\n mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {\n updateUser(id: $id, input: $input) {\n id\n }\n }\n": typeof types.UpdateUserDocument,
5353
"\n mutation DeleteUser($id: ID!) {\n deleteUser(id: $id)\n }\n": typeof types.DeleteUserDocument,
54+
"\n mutation LogoutUserDevices($userID: ID!) {\n logoutUser(userID: $userID)\n }\n": typeof types.LogoutUserDevicesDocument,
5455
"\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": typeof types.UserByIdDocument,
5556
"\n query GroupList {\n groups {\n id\n name\n }\n }\n": typeof types.GroupListDocument,
5657
"\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.UsersTableDocument,
@@ -96,6 +97,7 @@ const documents: Documents = {
9697
"\n query UserAuditInfo($id: ID!) {\n user(id: $id) {\n id\n createdAt\n updatedAt\n }\n }\n": types.UserAuditInfoDocument,
9798
"\n mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {\n updateUser(id: $id, input: $input) {\n id\n }\n }\n": types.UpdateUserDocument,
9899
"\n mutation DeleteUser($id: ID!) {\n deleteUser(id: $id)\n }\n": types.DeleteUserDocument,
100+
"\n mutation LogoutUserDevices($userID: ID!) {\n logoutUser(userID: $userID)\n }\n": types.LogoutUserDevicesDocument,
99101
"\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": types.UserByIdDocument,
100102
"\n query GroupList {\n groups {\n id\n name\n }\n }\n": types.GroupListDocument,
101103
"\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.UsersTableDocument,
@@ -266,6 +268,10 @@ export function graphql(source: "\n mutation UpdateUser($id: ID!, $input: Updat
266268
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
267269
*/
268270
export function graphql(source: "\n mutation DeleteUser($id: ID!) {\n deleteUser(id: $id)\n }\n"): (typeof documents)["\n mutation DeleteUser($id: ID!) {\n deleteUser(id: $id)\n }\n"];
271+
/**
272+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
273+
*/
274+
export function graphql(source: "\n mutation LogoutUserDevices($userID: ID!) {\n logoutUser(userID: $userID)\n }\n"): (typeof documents)["\n mutation LogoutUserDevices($userID: ID!) {\n logoutUser(userID: $userID)\n }\n"];
269275
/**
270276
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
271277
*/

gql/graphql.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,6 @@ export type Mutation = {
285285
deleteDatabase: Scalars['Boolean']['output'];
286286
/** Delete a group. */
287287
deleteGroup: Scalars['Boolean']['output'];
288-
/** Delete the current user. */
289-
deleteMe: Scalars['Boolean']['output'];
290288
/** Delete a question. */
291289
deleteQuestion: Scalars['Boolean']['output'];
292290
/** Delete a scope set. */
@@ -302,6 +300,8 @@ export type Mutation = {
302300
impersonateUser: Scalars['String']['output'];
303301
/** Logout from all the devices of the current user. */
304302
logoutAll: Scalars['Boolean']['output'];
303+
/** Logout a user from all his devices. */
304+
logoutUser: Scalars['Boolean']['output'];
305305
/** Update a database. */
306306
updateDatabase: Database;
307307
/** Update a group. */
@@ -369,6 +369,11 @@ export type MutationImpersonateUserArgs = {
369369
};
370370

371371

372+
export type MutationLogoutUserArgs = {
373+
userID: Scalars['ID']['input'];
374+
};
375+
376+
372377
export type MutationUpdateDatabaseArgs = {
373378
id: Scalars['ID']['input'];
374379
input: UpdateDatabaseInput;
@@ -1203,6 +1208,13 @@ export type DeleteUserMutationVariables = Exact<{
12031208

12041209
export type DeleteUserMutation = { __typename?: 'Mutation', deleteUser: boolean };
12051210

1211+
export type LogoutUserDevicesMutationVariables = Exact<{
1212+
userID: Scalars['ID']['input'];
1213+
}>;
1214+
1215+
1216+
export type LogoutUserDevicesMutation = { __typename?: 'Mutation', logoutUser: boolean };
1217+
12061218
export type UserByIdQueryVariables = Exact<{
12071219
id: Scalars['ID']['input'];
12081220
}>;
@@ -1280,6 +1292,7 @@ export const UserGroupsDocument = {"kind":"Document","definitions":[{"kind":"Ope
12801292
export const UserAuditInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserAuditInfo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<UserAuditInfoQuery, UserAuditInfoQueryVariables>;
12811293
export const UpdateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<UpdateUserMutation, UpdateUserMutationVariables>;
12821294
export const DeleteUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<DeleteUserMutation, DeleteUserMutationVariables>;
1295+
export const LogoutUserDevicesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"LogoutUserDevices"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logoutUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userID"}}}]}]}}]} as unknown as DocumentNode<LogoutUserDevicesMutation, LogoutUserDevicesMutationVariables>;
12831296
export const UserByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<UserByIdQuery, UserByIdQueryVariables>;
12841297
export const GroupListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GroupList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groups"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GroupListQuery, GroupListQueryVariables>;
12851298
export const UsersTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode<UsersTableQuery, UsersTableQueryVariables>;

schema.graphql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,6 @@ type Mutation {
284284
deleteDatabase(id: ID!): Boolean!
285285
"""Delete a group."""
286286
deleteGroup(id: ID!): Boolean!
287-
"""Delete the current user."""
288-
deleteMe: Boolean!
289287
"""Delete a question."""
290288
deleteQuestion(id: ID!): Boolean!
291289
"""Delete a scope set."""
@@ -301,6 +299,8 @@ type Mutation {
301299
impersonateUser(userID: ID!): String!
302300
"""Logout from all the devices of the current user."""
303301
logoutAll: Boolean!
302+
"""Logout a user from all his devices."""
303+
logoutUser(userID: ID!): Boolean!
304304
"""Update a database."""
305305
updateDatabase(id: ID!, input: UpdateDatabaseInput!): Database!
306306
"""Update a group."""

0 commit comments

Comments
 (0)