feat: support admin create new user account
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
47
app/api/user/admin/add/route.ts
Normal file
47
app/api/user/admin/add/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { checkUserStatus, getAllUsers } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,6 +33,8 @@ description: 如何配置项目中的邮件服务
|
||||
|
||||
如果你还没有 Resend 账号,请按照 [这里](https://resend.com/signup) 的注册流程操作。
|
||||
|
||||
> Resend 免费账号提供每天发送 100 个邮件额度,绑定 1 个域名,足够一般用户使用。
|
||||
|
||||
### 创建 API 密钥
|
||||
|
||||
登录 Resend 后,它会提示你创建第一个 API 密钥。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user