Skip to content

Commit

Permalink
feat: 🎸 add user card
Browse files Browse the repository at this point in the history
  • Loading branch information
s-hirano-ist committed Oct 14, 2024
1 parent 1ec7fe4 commit 1ac37cf
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 5 deletions.
31 changes: 29 additions & 2 deletions src/apis/prisma/fetch-user.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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 },
});
}
25 changes: 25 additions & 0 deletions src/app/admin/@users/page.tsx
Original file line number Diff line number Diff line change
@@ -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 ? (
<div className="space-y-2">
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
) : (
<Unauthorized />
)}
</>
);
}
7 changes: 6 additions & 1 deletion src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<Header title={displayName} />
Expand All @@ -24,8 +25,12 @@ export default async function Layout({ dumper }: Props) {
<TabsTrigger className="w-full" value="dumper">
DUMPER
</TabsTrigger>
<TabsTrigger className="w-full" value="users">
USERS
</TabsTrigger>
</TabsList>
<TabsContent value="dumper"> {dumper}</TabsContent>
<TabsContent value="users"> {users}</TabsContent>
</Tabs>
</>
);
Expand Down
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Role } from "@prisma/client";

export const PAGE_NAME = "private.s-hirano.com";

export const FORM_ERROR_MESSAGES = {
Expand All @@ -22,6 +24,7 @@ export const ERROR_MESSAGES = {
export const SUCCESS_MESSAGES = {
INSERTED: "正常に登録できました。",
SCOPE_UPDATED: "スコープを正常に変更しました。",
ROLE_UPDATED: "ロールを正常に更新しました。",
PROFILE_UPSERTED: "プロフィールを更新しました。",
SIGN_IN: "サインインに成功しました。",
SIGN_OUT: "サインアウトに成功しました。",
Expand All @@ -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"];
32 changes: 32 additions & 0 deletions src/features/auth/actions/change-role.ts
Original file line number Diff line number Diff line change
@@ -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<ServerAction<undefined>> {
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);
}
}
55 changes: 55 additions & 0 deletions src/features/auth/components/role-update-selector.tsx
Original file line number Diff line number Diff line change
@@ -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>(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 (
<Select value={value} onValueChange={handleScopeChange}>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ROLES.map((role) => (
<SelectItem key={role} value={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
23 changes: 23 additions & 0 deletions src/features/auth/components/user-card.tsx
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof getUsers>>[0];
};

export function UserCard({ user }: Props) {
return (
<div className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<p className="text-base font-bold text-primary">ユーザー情報</p>
<p>{user.id}</p>
<p>{user.username}</p>
</div>
<div>
<RoleUpdateSelector userId={user.id} role={user.role} />
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ type Action = {
export type ServerAction<T> =
| (Action & { success: true; data: T })
| (Action & { success: false });

export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
8 changes: 6 additions & 2 deletions src/utils/format-for-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T extends Promise<infer U> ? U : T;
import type { UnwrapPromise } from "@/types";
import type { Role, Scope } from "@prisma/client";

export function formatChangeStatusMessage(
status: Status,
Expand Down Expand Up @@ -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に変更しました`;
}

0 comments on commit 1ac37cf

Please sign in to comment.