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