diff --git a/src/apis/prisma/fetch-user.ts b/src/apis/prisma/fetch-user.ts index f78483d..b713f5b 100644 --- a/src/apis/prisma/fetch-user.ts +++ b/src/apis/prisma/fetch-user.ts @@ -1,8 +1,8 @@ import "server-only"; import { NotAllowedError } from "@/error-classes"; -import { getUserId } from "@/features/auth/utils/get-session"; +import { getSelfRole, getUserId } from "@/features/auth/utils/get-session"; import prisma from "@/prisma"; -import type { Scope } from "@prisma/client"; +import type { Role, Scope } from "@prisma/client"; // everyone can access export async function getUserScope(username: string) { @@ -75,3 +75,30 @@ export async function getNewsAndContents(username: string) { return newsAndContents; } + +// ROLE === "admin" only +export async function getUsers() { + const role = await getSelfRole(); + + if (role !== "ADMIN") throw new NotAllowedError(); + + return await prisma.users.findMany({ + select: { + id: true, + username: true, + role: true, + Profile: true, + }, + }); +} + +export async function updateRole(userId: string, role: Role) { + const selfRole = await getSelfRole(); + + if (selfRole !== "ADMIN") throw new NotAllowedError(); + + return await prisma.users.update({ + where: { id: userId }, + data: { role }, + }); +} diff --git a/src/app/admin/@users/page.tsx b/src/app/admin/@users/page.tsx new file mode 100644 index 0000000..4654640 --- /dev/null +++ b/src/app/admin/@users/page.tsx @@ -0,0 +1,25 @@ +import { getUsers } from "@/apis/prisma/fetch-user"; +import { Unauthorized } from "@/components/unauthorized"; +import { UserCard } from "@/features/auth/components/user-card"; +import { checkAdminPermission } from "@/features/auth/utils/role"; +export const dynamic = "force-dynamic"; + +export default async function Page() { + const hasAdminPermission = await checkAdminPermission(); + + const users = await getUsers(); + + return ( + <> + {hasAdminPermission ? ( +
+ {users.map((user) => ( + + ))} +
+ ) : ( + + )} + + ); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 76864a4..433e1df 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -13,9 +13,10 @@ export const metadata: Metadata = { type Props = { dumper: ReactNode; + users: ReactNode; }; -export default async function Layout({ dumper }: Props) { +export default async function Layout({ dumper, users }: Props) { return ( <>
@@ -24,8 +25,12 @@ export default async function Layout({ dumper }: Props) { DUMPER + + USERS + {dumper} + {users} ); diff --git a/src/constants.ts b/src/constants.ts index d04e60c..6a78bd2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ +import type { Role } from "@prisma/client"; + export const PAGE_NAME = "private.s-hirano.com"; export const FORM_ERROR_MESSAGES = { @@ -22,6 +24,7 @@ export const ERROR_MESSAGES = { export const SUCCESS_MESSAGES = { INSERTED: "正常に登録できました。", SCOPE_UPDATED: "スコープを正常に変更しました。", + ROLE_UPDATED: "ロールを正常に更新しました。", PROFILE_UPSERTED: "プロフィールを更新しました。", SIGN_IN: "サインインに成功しました。", SIGN_OUT: "サインアウトに成功しました。", @@ -43,3 +46,5 @@ export const UTIL_URLS = [ { name: "PORTAINER", url: "https://private.s-hirano.com:9443" }, { name: "GRAFANA", url: "https://private.s-hirano.com:3001" }, ] as const; + +export const ROLES: Role[] = ["ADMIN", "EDITOR", "VIEWER", "UNAUTHORIZED"]; diff --git a/src/features/auth/actions/change-role.ts b/src/features/auth/actions/change-role.ts new file mode 100644 index 0000000..2358610 --- /dev/null +++ b/src/features/auth/actions/change-role.ts @@ -0,0 +1,32 @@ +"use server"; +import "server-only"; +import { sendLineNotifyMessage } from "@/apis/line-notify/fetch-message"; +import { updateRole } from "@/apis/prisma/fetch-user"; +import { SUCCESS_MESSAGES } from "@/constants"; +import { NotAllowedError } from "@/error-classes"; +import { wrapServerSideErrorForClient } from "@/error-wrapper"; +import type { ServerAction } from "@/types"; +import { formatUpdateRoleMessage } from "@/utils/format-for-line"; +import type { Role } from "@prisma/client"; +import { getSelfRole } from "../utils/get-session"; + +export async function changeRole( + userId: string, + role: Role, +): Promise> { + try { + const selfRole = await getSelfRole(); + if (selfRole !== "ADMIN") throw new NotAllowedError(); + + await updateRole(userId, role); + await sendLineNotifyMessage(formatUpdateRoleMessage(role)); + + return { + success: true, + message: SUCCESS_MESSAGES.ROLE_UPDATED, + data: undefined, + }; + } catch (error) { + return await wrapServerSideErrorForClient(error); + } +} diff --git a/src/features/auth/components/role-update-selector.tsx b/src/features/auth/components/role-update-selector.tsx new file mode 100644 index 0000000..58f6fb5 --- /dev/null +++ b/src/features/auth/components/role-update-selector.tsx @@ -0,0 +1,55 @@ +"use client"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ROLES } from "@/constants"; +import { useToast } from "@/hooks/use-toast"; +import type { Role } from "@prisma/client"; +import { useState } from "react"; +import { changeRole } from "../actions/change-role"; + +type Props = { + userId: string; + role: Role; +}; + +export function RoleUpdateSelector({ userId, role }: Props) { + const { toast } = useToast(); + + const [value, setValue] = useState(role); + + async function handleScopeChange(value: Role) { + const response = await changeRole(userId, value); + if (!response.success) { + toast({ + variant: "destructive", + description: response.message, + }); + return; + } + toast({ + variant: "default", + description: response.message, + }); + setValue(value); + } + + return ( + + ); +} diff --git a/src/features/auth/components/user-card.tsx b/src/features/auth/components/user-card.tsx new file mode 100644 index 0000000..a2fc040 --- /dev/null +++ b/src/features/auth/components/user-card.tsx @@ -0,0 +1,23 @@ +"use client"; +import type { getUsers } from "@/apis/prisma/fetch-user"; +import type { UnwrapPromise } from "@/types"; +import { RoleUpdateSelector } from "./role-update-selector"; + +type Props = { + user: UnwrapPromise>[0]; +}; + +export function UserCard({ user }: Props) { + return ( +
+
+

ユーザー情報

+

{user.id}

+

{user.username}

+
+
+ +
+
+ ); +} diff --git a/src/types.ts b/src/types.ts index 70cd29b..af4ec7c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,3 +9,5 @@ type Action = { export type ServerAction = | (Action & { success: true; data: T }) | (Action & { success: false }); + +export type UnwrapPromise = T extends Promise ? U : T; diff --git a/src/utils/format-for-line.ts b/src/utils/format-for-line.ts index 5110c97..b30a626 100644 --- a/src/utils/format-for-line.ts +++ b/src/utils/format-for-line.ts @@ -3,8 +3,8 @@ import type { createSelfNews } from "@/apis/prisma/fetch-news"; import type { ContentName } from "@/features/dump/types"; import type { ProfileSchema } from "@/features/profile/schemas/profile-schema"; import type { Status } from "@/features/update-status/types"; -import type { Scope } from "@prisma/client"; -type UnwrapPromise = T extends Promise ? U : T; +import type { UnwrapPromise } from "@/types"; +import type { Role, Scope } from "@prisma/client"; export function formatChangeStatusMessage( status: Status, @@ -44,3 +44,7 @@ export function formatUpdateScopeMessage(scope: Scope) { export function formatUpsertProfileMessage(data: ProfileSchema) { return `【PROFILE】\n\nname: ${data.name}\nに変更しました`; } + +export function formatUpdateRoleMessage(role: Role) { + return `【ROLE】\n\nrole: ${role}\nに変更しました`; +}