feat: support admin create new user account

This commit is contained in:
oiov
2025-06-26 16:53:42 +08:00
parent c1743c2840
commit 8aa4602390
12 changed files with 106 additions and 19 deletions

View File

@@ -34,7 +34,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { UserForm } from "@/components/forms/user-form"; import { FormType, UserForm } from "@/components/forms/user-form";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
import { Icons } from "@/components/shared/icons"; import { Icons } from "@/components/shared/icons";
import { PaginationWrapper } from "@/components/shared/pagination"; import { PaginationWrapper } from "@/components/shared/pagination";
@@ -74,6 +74,7 @@ function TableColumnSekleton({ className }: { className?: string }) {
export default function UsersList({ user }: UrlListProps) { export default function UsersList({ user }: UrlListProps) {
const { isMobile } = useMediaQuery(); const { isMobile } = useMediaQuery();
const [formType, setFormType] = useState<FormType>("add");
const [isShowForm, setShowForm] = useState(false); const [isShowForm, setShowForm] = useState(false);
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null); const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@@ -121,6 +122,19 @@ export default function UsersList({ user }: UrlListProps) {
<RefreshCwIcon className="size-4" /> <RefreshCwIcon className="size-4" />
)} )}
</Button> </Button>
<Button
className="flex shrink-0 gap-1"
variant="default"
onClick={() => {
setcurrentEditUser(null);
setShowForm(false);
setFormType("add");
setShowForm(!isShowForm);
}}
>
<Icons.add className="size-4" />
<span className="hidden sm:inline">{t("Add User")}</span>
</Button>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -262,6 +276,7 @@ export default function UsersList({ user }: UrlListProps) {
onClick={() => { onClick={() => {
setcurrentEditUser(user); setcurrentEditUser(user);
setShowForm(false); setShowForm(false);
setFormType("edit");
setShowForm(!isShowForm); setShowForm(!isShowForm);
}} }}
> >
@@ -304,7 +319,7 @@ export default function UsersList({ user }: UrlListProps) {
user={{ id: user.id, name: user.name || "" }} user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm} isShowForm={isShowForm}
setShowForm={setShowForm} setShowForm={setShowForm}
type="edit" type={formType}
initData={currentEditUser} initData={currentEditUser}
onRefresh={handleRefresh} onRefresh={handleRefresh}
/> />

View File

@@ -1,6 +1,7 @@
import { NextRequest } from "next/server"; import { NextRequest } from "next/server";
import { prisma } from "@/lib/db"; import { prisma } from "@/lib/db";
import { getMultipleConfigs } from "@/lib/dto/system-config";
import { hashPassword, verifyPassword } from "@/lib/utils"; import { hashPassword, verifyPassword } from "@/lib/utils";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
@@ -17,6 +18,10 @@ export async function POST(req: NextRequest) {
}); });
if (!user) { if (!user) {
const configs = await getMultipleConfigs(["enable_user_registration"]);
if (!configs.enable_user_registration) {
return Response.json("User registration is disabled", { status: 403 });
}
const newUser = await prisma.user.create({ const newUser = await prisma.user.create({
data: { data: {
name: "", name: "",

View File

@@ -0,0 +1,47 @@
import { prisma } from "@/lib/db";
import { checkUserStatus } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
import { hashPassword } from "@/lib/utils";
export async function POST(req: Request) {
try {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
if (user.role !== "ADMIN") {
return Response.json("Unauthorized", {
status: 401,
});
}
const { email, password, name, team } = await req.json();
if (!email || !password) {
return Response.json("email and password is required", { status: 400 });
}
const has_user = await prisma.user.findUnique({
where: {
email,
},
});
if (has_user) {
return Response.json("User already exists", { status: 400 });
}
const newUser = await prisma.user.create({
data: {
name,
email,
password: hashPassword(password),
active: 1,
role: "USER",
team,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
});
return Response.json(newUser.id, { status: 200 });
} catch (error) {
return Response.json({ statusText: "Server error" }, { status: 500 });
}
}

View File

@@ -1,4 +1,3 @@
import { env } from "@/env.mjs";
import { checkUserStatus, getAllUsers } from "@/lib/dto/user"; import { checkUserStatus, getAllUsers } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session"; import { getCurrentUser } from "@/lib/session";

View File

@@ -547,8 +547,8 @@ export default function EmailSidebar({
</div> </div>
<span className="line-clamp-1 hover:line-clamp-none"> <span className="line-clamp-1 hover:line-clamp-none">
{isAdminModel {isAdminModel
? `Created by ${email.user || email.email.slice(0, 5)} at` ? `${email.user || email.email.slice(0, 5)} · `
: ""}{" "} : ""}
<TimeAgoIntl date={email.createdAt} /> <TimeAgoIntl date={email.createdAt} />
</span> </span>
</div> </div>

View File

@@ -221,12 +221,7 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
<Button <Button
className="my-2" className="my-2"
disabled={ disabled={isLoading || isGoogleLoading || isGithubLoading}
!loginMethod.registration ||
isLoading ||
isGoogleLoading ||
isGithubLoading
}
> >
{isLoading && ( {isLoading && (
<Icons.spinner className="mr-2 size-4 animate-spin" /> <Icons.spinner className="mr-2 size-4 animate-spin" />

View File

@@ -29,7 +29,7 @@ import { Switch } from "../ui/switch";
export type FormData = User; export type FormData = User;
export type FormType = "edit"; export type FormType = "add" | "edit";
export interface RecordFormProps { export interface RecordFormProps {
user: Pick<User, "id" | "name">; user: Pick<User, "id" | "name">;
@@ -81,9 +81,29 @@ export function UserForm({
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
if (type === "edit") { if (type === "edit") {
handleUpdate(data); handleUpdate(data);
} else if (type === "add") {
handleCreate(data);
} }
}); });
const handleCreate = async (data: User) => {
startTransition(async () => {
const response = await fetch("/api/user/admin/add", {
method: "POST",
body: JSON.stringify(data),
});
if (!response.ok || response.status !== 200) {
toast.error("Create Failed", {
description: response.statusText,
});
} else {
toast.success(`Create successfully!`);
setShowForm(false);
onRefresh();
}
});
};
const handleUpdate = async (data: User) => { const handleUpdate = async (data: User) => {
startTransition(async () => { startTransition(async () => {
if (type === "edit") { if (type === "edit") {
@@ -127,7 +147,7 @@ export function UserForm({
return ( return (
<div> <div>
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold"> <div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
{t("Edit User")} {type === "add" ? t("Add User") : t("Edit User")}
</div> </div>
<form className="max-w-2xl p-4" onSubmit={onSubmit}> <form className="max-w-2xl p-4" onSubmit={onSubmit}>
<div className="items-center justify-start gap-4 md:flex"> <div className="items-center justify-start gap-4 md:flex">
@@ -140,7 +160,7 @@ export function UserForm({
id="email" id="email"
className="flex-1 shadow-inner" className="flex-1 shadow-inner"
size={32} size={32}
disabled disabled={type === "edit"}
{...register("email")} {...register("email")}
/> />
</div> </div>

View File

@@ -33,6 +33,8 @@ description: 如何配置项目中的邮件服务
如果你还没有 Resend 账号,请按照 [这里](https://resend.com/signup) 的注册流程操作。 如果你还没有 Resend 账号,请按照 [这里](https://resend.com/signup) 的注册流程操作。
> Resend 免费账号提供每天发送 100 个邮件额度,绑定 1 个域名,足够一般用户使用。
### 创建 API 密钥 ### 创建 API 密钥
登录 Resend 后,它会提示你创建第一个 API 密钥。 登录 Resend 后,它会提示你创建第一个 API 密钥。

View File

@@ -34,6 +34,8 @@ The following will demonstrate how to configure the Resend key required for logi
If don't have an account on Resend, just follow their steps after signup [here](https://resend.com/signup). If don't have an account on Resend, just follow their steps after signup [here](https://resend.com/signup).
> Resend's free account offers a daily email limit of 100 emails, bound to 1 domain name, which is sufficient for ordinary users.
### Create an API key ### Create an API key
After signin on Resend, he propurse you to create your first API key. After signin on Resend, he propurse you to create your first API key.

View File

@@ -184,7 +184,8 @@
"Login Password": "Password", "Login Password": "Password",
"Duplicate": "Duplicate", "Duplicate": "Duplicate",
"Confirm duplicate domain": "Confirm duplicate domain", "Confirm duplicate domain": "Confirm duplicate domain",
"This will duplicate all configuration information for the {domain} domain, and create a new domain": "This will duplicate all configuration information for the {domain} domain, and create a new domain" "This will duplicate all configuration information for the {domain} domain, and create a new domain": "This will duplicate all configuration information for the {domain} domain, and create a new domain",
"Add User": "Add User"
}, },
"Components": { "Components": {
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
@@ -360,7 +361,7 @@
"Email Code": "Email", "Email Code": "Email",
"Password": "Password", "Password": "Password",
"Sign In / Sign Up": "Sign In / Sign Up", "Sign In / Sign Up": "Sign In / Sign Up",
"Incorrect email or password": "Incorrect email or password", "Incorrect email or password": "Incorrect email or password, or administrator closed new user registration",
"Something went wrong": "Something went wrong", "Something went wrong": "Something went wrong",
"Check your email": "Check your email", "Check your email": "Check your email",
"We sent you a login link": "We sent you a login link", "We sent you a login link": "We sent you a login link",

View File

@@ -184,7 +184,8 @@
"Login Password": "用户密码", "Login Password": "用户密码",
"Duplicate": "复制", "Duplicate": "复制",
"Confirm duplicate domain": "确认复制域名", "Confirm duplicate domain": "确认复制域名",
"This will duplicate all configuration information for the {domain} domain, and create a new domain": "这将复制 {domain} 域名的所有配置信息,并创建一个新域名" "This will duplicate all configuration information for the {domain} domain, and create a new domain": "这将复制 {domain} 域名的所有配置信息,并创建一个新域名",
"Add User": "添加用户"
}, },
"Components": { "Components": {
"Dashboard": "用户面板", "Dashboard": "用户面板",
@@ -360,7 +361,7 @@
"Email Code": "邮箱验证", "Email Code": "邮箱验证",
"Password": "账号密码", "Password": "账号密码",
"Sign In / Sign Up": "点击登录/注册", "Sign In / Sign Up": "点击登录/注册",
"Incorrect email or password": "邮箱或密码错误", "Incorrect email or password": "邮箱或密码错误,或管理员关闭了新用户注册",
"Something went wrong": "出错了", "Something went wrong": "出错了",
"Check your email": "检查您的邮箱", "Check your email": "检查您的邮箱",
"We sent you a login link": "我们已向您发送登录链接", "We sent you a login link": "我们已向您发送登录链接",

File diff suppressed because one or more lines are too long