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,
TooltipTrigger,
} 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 { Icons } from "@/components/shared/icons";
import { PaginationWrapper } from "@/components/shared/pagination";
@@ -74,6 +74,7 @@ function TableColumnSekleton({ className }: { className?: string }) {
export default function UsersList({ user }: UrlListProps) {
const { isMobile } = useMediaQuery();
const [formType, setFormType] = useState<FormType>("add");
const [isShowForm, setShowForm] = useState(false);
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1);
@@ -121,6 +122,19 @@ export default function UsersList({ user }: UrlListProps) {
<RefreshCwIcon className="size-4" />
)}
</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>
</CardHeader>
<CardContent>
@@ -262,6 +276,7 @@ export default function UsersList({ user }: UrlListProps) {
onClick={() => {
setcurrentEditUser(user);
setShowForm(false);
setFormType("edit");
setShowForm(!isShowForm);
}}
>
@@ -304,7 +319,7 @@ export default function UsersList({ user }: UrlListProps) {
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type="edit"
type={formType}
initData={currentEditUser}
onRefresh={handleRefresh}
/>

View File

@@ -1,6 +1,7 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { getMultipleConfigs } from "@/lib/dto/system-config";
import { hashPassword, verifyPassword } from "@/lib/utils";
export async function POST(req: NextRequest) {
@@ -17,6 +18,10 @@ export async function POST(req: NextRequest) {
});
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({
data: {
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 { getCurrentUser } from "@/lib/session";

View File

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

View File

@@ -221,12 +221,7 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
<Button
className="my-2"
disabled={
!loginMethod.registration ||
isLoading ||
isGoogleLoading ||
isGithubLoading
}
disabled={isLoading || isGoogleLoading || isGithubLoading}
>
{isLoading && (
<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 FormType = "edit";
export type FormType = "add" | "edit";
export interface RecordFormProps {
user: Pick<User, "id" | "name">;
@@ -81,9 +81,29 @@ export function UserForm({
const onSubmit = handleSubmit((data) => {
if (type === "edit") {
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) => {
startTransition(async () => {
if (type === "edit") {
@@ -127,7 +147,7 @@ export function UserForm({
return (
<div>
<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>
<form className="max-w-2xl p-4" onSubmit={onSubmit}>
<div className="items-center justify-start gap-4 md:flex">
@@ -140,7 +160,7 @@ export function UserForm({
id="email"
className="flex-1 shadow-inner"
size={32}
disabled
disabled={type === "edit"}
{...register("email")}
/>
</div>

View File

@@ -33,6 +33,8 @@ description: 如何配置项目中的邮件服务
如果你还没有 Resend 账号,请按照 [这里](https://resend.com/signup) 的注册流程操作。
> Resend 免费账号提供每天发送 100 个邮件额度,绑定 1 个域名,足够一般用户使用。
### 创建 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).
> 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
After signin on Resend, he propurse you to create your first API key.

View File

@@ -184,7 +184,8 @@
"Login Password": "Password",
"Duplicate": "Duplicate",
"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": {
"Dashboard": "Dashboard",
@@ -360,7 +361,7 @@
"Email Code": "Email",
"Password": "Password",
"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",
"Check your email": "Check your email",
"We sent you a login link": "We sent you a login link",

View File

@@ -184,7 +184,8 @@
"Login Password": "用户密码",
"Duplicate": "复制",
"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": {
"Dashboard": "用户面板",
@@ -360,7 +361,7 @@
"Email Code": "邮箱验证",
"Password": "账号密码",
"Sign In / Sign Up": "点击登录/注册",
"Incorrect email or password": "邮箱或密码错误",
"Incorrect email or password": "邮箱或密码错误,或管理员关闭了新用户注册",
"Something went wrong": "出错了",
"Check your email": "检查您的邮箱",
"We sent you a login link": "我们已向您发送登录链接",

File diff suppressed because one or more lines are too long