mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: add user role field (#49)
* feat: add user role field * chore: fix typo * feat: update signup api
This commit is contained in:
@@ -15,7 +15,6 @@ const validateConfig: ValidatorConfig = {
|
||||
interface Props extends DialogProps {}
|
||||
|
||||
const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPasswordAgain, setNewPasswordAgain] = useState("");
|
||||
|
||||
@@ -27,11 +26,6 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleOldPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value as string;
|
||||
setOldPassword(text);
|
||||
};
|
||||
|
||||
const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value as string;
|
||||
setNewPassword(text);
|
||||
@@ -43,7 +37,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (oldPassword === "" || newPassword === "" || newPasswordAgain === "") {
|
||||
if (newPassword === "" || newPasswordAgain === "") {
|
||||
toastHelper.error("Please fill in all fields.");
|
||||
return;
|
||||
}
|
||||
@@ -61,14 +55,6 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const isValid = await userService.checkPasswordValid(oldPassword);
|
||||
|
||||
if (!isValid) {
|
||||
toastHelper.error("Old password is invalid.");
|
||||
setOldPassword("");
|
||||
return;
|
||||
}
|
||||
|
||||
await userService.updatePassword(newPassword);
|
||||
toastHelper.info("Password changed.");
|
||||
handleCloseBtnClick();
|
||||
@@ -86,16 +72,12 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<label className="form-label input-form-label">
|
||||
<span className={"normal-text " + (oldPassword === "" ? "" : "not-null")}>Old password</span>
|
||||
<input type="password" value={oldPassword} onChange={handleOldPasswordChanged} />
|
||||
</label>
|
||||
<label className="form-label input-form-label">
|
||||
<span className={"normal-text " + (newPassword === "" ? "" : "not-null")}>New passworld</span>
|
||||
<input type="password" value={newPassword} onChange={handleNewPasswordChanged} />
|
||||
</label>
|
||||
<label className="form-label input-form-label">
|
||||
<span className={"normal-text " + (newPasswordAgain === "" ? "" : "not-null")}>New password again</span>
|
||||
<span className={"normal-text " + (newPasswordAgain === "" ? "" : "not-null")}>Repeat the new password</span>
|
||||
<input type="password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
|
||||
</label>
|
||||
<div className="btns-container">
|
||||
|
@@ -40,13 +40,6 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const isUsable = await userService.checkUsernameUsable(username);
|
||||
|
||||
if (!isUsable) {
|
||||
toastHelper.error("Username is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
await userService.updateUsername(username);
|
||||
await userService.doSignIn();
|
||||
toastHelper.info("Username changed");
|
||||
@@ -80,6 +73,10 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
<span className="normal-text">Created at:</span>
|
||||
<span className="normal-text">{utils.getDateString(user.createdAt)}</span>
|
||||
</label>
|
||||
<label className="form-label">
|
||||
<span className="normal-text">Email:</span>
|
||||
<span className="normal-text">{user.email}</span>
|
||||
</label>
|
||||
<label className="form-label input-form-label username-label">
|
||||
<span className="normal-text">Username:</span>
|
||||
<input type="text" value={username} onChange={handleUsernameChanged} />
|
||||
|
@@ -39,6 +39,13 @@ async function request<T>(config: RequestConfig): Promise<T> {
|
||||
}
|
||||
|
||||
namespace api {
|
||||
export function getSystemStatus() {
|
||||
return request<API.SystemStatus>({
|
||||
method: "GET",
|
||||
url: "/api/status",
|
||||
});
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return request<Model.User>({
|
||||
method: "GET",
|
||||
@@ -46,22 +53,24 @@ namespace api {
|
||||
});
|
||||
}
|
||||
|
||||
export function login(name: string, password: string) {
|
||||
export function login(email: string, password: string) {
|
||||
return request<Model.User>({
|
||||
method: "POST",
|
||||
url: "/api/auth/login",
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function signup(name: string, password: string) {
|
||||
export function signup(email: string, role: UserRole, name: string, password: string) {
|
||||
return request<Model.User>({
|
||||
method: "POST",
|
||||
url: "/api/auth/signup",
|
||||
data: {
|
||||
email,
|
||||
role,
|
||||
name,
|
||||
password,
|
||||
},
|
||||
|
@@ -18,11 +18,11 @@
|
||||
|
||||
> .page-content-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply flex-nowrap;
|
||||
@apply w-full;
|
||||
|
||||
> .form-item-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply relative w-full text-base m-2;
|
||||
@apply relative w-full text-base my-2;
|
||||
|
||||
> .normal-text {
|
||||
@apply absolute top-3 left-3 px-1 leading-10 flex-shrink-0 text-base cursor-text text-gray-400 bg-transparent transition-all select-none;
|
||||
@@ -46,62 +46,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
> .page-footer-container {
|
||||
.flex(row, space-between, center);
|
||||
@apply w-full mt-3;
|
||||
|
||||
> .btns-container {
|
||||
.flex(row, flex-start, center);
|
||||
|
||||
> .btn {
|
||||
@apply px-1 py-2 text-sm rounded;
|
||||
|
||||
&:hover {
|
||||
@apply opacity-80;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@apply text-gray-400 cursor-not-allowed;
|
||||
}
|
||||
|
||||
&.signin-btn {
|
||||
@apply bg-green-600 text-white px-3;
|
||||
|
||||
&.requesting {
|
||||
@apply cursor-wait opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .btn-text {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
> .split-text {
|
||||
@apply text-gray-400 mx-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .quickly-btns-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full mt-6;
|
||||
> .action-btns-container {
|
||||
.flex(row, flex-end, center);
|
||||
@apply w-full mt-2;
|
||||
|
||||
> .btn {
|
||||
@apply mb-6 text-base leading-10 border border-solid border-gray-400 px-4 rounded-3xl;
|
||||
@apply px-1 py-2 text-sm rounded;
|
||||
|
||||
&:hover {
|
||||
@apply opacity-80;
|
||||
}
|
||||
|
||||
&.guest-signin {
|
||||
@apply text-green-600 border-2 border-green-600 font-bold;
|
||||
&.disabled {
|
||||
@apply text-gray-400 cursor-not-allowed;
|
||||
}
|
||||
|
||||
&.requesting {
|
||||
@apply cursor-wait opacity-80;
|
||||
&.signin-btn {
|
||||
@apply bg-green-600 text-white px-3;
|
||||
|
||||
&.requesting {
|
||||
@apply cursor-wait opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .btn-text {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
> .split-text {
|
||||
@apply text-gray-400 mx-2;
|
||||
}
|
||||
}
|
||||
|
||||
> .tip-text {
|
||||
@apply w-full text-sm mt-4 text-gray-500 text-right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import api from "../helpers/api";
|
||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
@@ -16,31 +16,22 @@ const validateConfig: ValidatorConfig = {
|
||||
};
|
||||
|
||||
const Signin: React.FC<Props> = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const pageLoadingState = useLoading(true);
|
||||
const [siteOwner, setSiteOwner] = useState<Model.User>();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showAutoSigninAsGuest, setShowAutoSigninAsGuest] = useState(true);
|
||||
const signinBtnsClickLoadingState = useLoading(false);
|
||||
const autoSigninAsGuestBtn = useRef<HTMLDivElement>(null);
|
||||
const signinBtn = useRef<HTMLButtonElement>(null);
|
||||
const actionBtnLoadingState = useLoading(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
autoSigninAsGuestBtn.current?.click();
|
||||
signinBtn.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener("keypress", handleKeyPress);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener("keypress", handleKeyPress);
|
||||
};
|
||||
api.getSystemStatus().then((status) => {
|
||||
setSiteOwner(status.owner);
|
||||
pageLoadingState.setFinish();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value as string;
|
||||
setUsername(text);
|
||||
setEmail(text);
|
||||
};
|
||||
|
||||
const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -48,14 +39,14 @@ const Signin: React.FC<Props> = () => {
|
||||
setPassword(text);
|
||||
};
|
||||
|
||||
const handleSigninBtnsClick = async (action: "signin" | "signup" = "signin") => {
|
||||
if (signinBtnsClickLoadingState.isLoading) {
|
||||
const handleSigninBtnsClick = async () => {
|
||||
if (actionBtnLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usernameValidResult = validate(username, validateConfig);
|
||||
if (!usernameValidResult.result) {
|
||||
toastHelper.error("Username: " + usernameValidResult.reason);
|
||||
const emailValidResult = validate(email, validateConfig);
|
||||
if (!emailValidResult.result) {
|
||||
toastHelper.error("Email: " + emailValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,13 +57,8 @@ const Signin: React.FC<Props> = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
signinBtnsClickLoadingState.setLoading();
|
||||
let actionFunc = api.login;
|
||||
if (action === "signup") {
|
||||
actionFunc = api.signup;
|
||||
}
|
||||
await actionFunc(username, password);
|
||||
|
||||
actionBtnLoadingState.setLoading();
|
||||
await api.login(email, password);
|
||||
const user = await userService.doSignIn();
|
||||
if (user) {
|
||||
locationService.replaceHistory("/");
|
||||
@@ -83,25 +69,52 @@ const Signin: React.FC<Props> = () => {
|
||||
console.error(error);
|
||||
toastHelper.error("😟 " + error.message);
|
||||
}
|
||||
signinBtnsClickLoadingState.setFinish();
|
||||
actionBtnLoadingState.setFinish();
|
||||
};
|
||||
|
||||
const handleSwitchAccountSigninBtnClick = () => {
|
||||
if (signinBtnsClickLoadingState.isLoading) {
|
||||
const handleSignUpAsOwnerBtnsClick = async () => {
|
||||
if (actionBtnLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShowAutoSigninAsGuest(false);
|
||||
const emailValidResult = validate(email, validateConfig);
|
||||
if (!emailValidResult.result) {
|
||||
toastHelper.error("Email: " + emailValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordValidResult = validate(password, validateConfig);
|
||||
if (!passwordValidResult.result) {
|
||||
toastHelper.error("Password: " + passwordValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = email.split("@")[0];
|
||||
|
||||
try {
|
||||
actionBtnLoadingState.setLoading();
|
||||
await api.signup(email, "OWNER", name, password);
|
||||
const user = await userService.doSignIn();
|
||||
if (user) {
|
||||
locationService.replaceHistory("/");
|
||||
} else {
|
||||
toastHelper.error("😟 Signup failed");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error("😟 " + error.message);
|
||||
}
|
||||
actionBtnLoadingState.setFinish();
|
||||
};
|
||||
|
||||
const handleAutoSigninAsGuestBtnClick = async () => {
|
||||
if (signinBtnsClickLoadingState.isLoading) {
|
||||
if (actionBtnLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
signinBtnsClickLoadingState.setLoading();
|
||||
await api.login("guest", "secret");
|
||||
actionBtnLoadingState.setLoading();
|
||||
await api.login("guest@example.com", "secret");
|
||||
|
||||
const user = await userService.doSignIn();
|
||||
if (user) {
|
||||
@@ -113,7 +126,7 @@ const Signin: React.FC<Props> = () => {
|
||||
console.error(error);
|
||||
toastHelper.error("😟 " + error.message);
|
||||
}
|
||||
signinBtnsClickLoadingState.setFinish();
|
||||
actionBtnLoadingState.setFinish();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -124,64 +137,42 @@ const Signin: React.FC<Props> = () => {
|
||||
<span className="icon-text">✍️</span> Memos
|
||||
</p>
|
||||
</div>
|
||||
{showAutoSigninAsGuest ? (
|
||||
<>
|
||||
<div className="quickly-btns-container">
|
||||
<div
|
||||
ref={autoSigninAsGuestBtn}
|
||||
className={`btn guest-signin ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={handleAutoSigninAsGuestBtnClick}
|
||||
>
|
||||
👉 Quick login as a guest
|
||||
</div>
|
||||
<div
|
||||
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={handleSwitchAccountSigninBtnClick}
|
||||
>
|
||||
Sign in/up with account
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="page-content-container">
|
||||
<div className="form-item-container input-form-container">
|
||||
<span className={"normal-text " + (username === "" ? "" : "not-null")}>Username</span>
|
||||
<input type="text" autoComplete="off" value={username} onChange={handleUsernameInputChanged} />
|
||||
</div>
|
||||
<div className="form-item-container input-form-container">
|
||||
<span className={"normal-text " + (password === "" ? "" : "not-null")}>Password</span>
|
||||
<input type="password" autoComplete="off" value={password} onChange={handlePasswordInputChanged} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-footer-container">
|
||||
<div className="btns-container">{/* nth */}</div>
|
||||
<div className="btns-container">
|
||||
<button
|
||||
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={handleAutoSigninAsGuestBtnClick}
|
||||
>
|
||||
Login as Guest
|
||||
</button>
|
||||
<span className="split-text">/</span>
|
||||
<button
|
||||
className={`btn signup-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={() => handleSigninBtnsClick("signup")}
|
||||
>
|
||||
Sign up
|
||||
</button>
|
||||
<span className="split-text">/</span>
|
||||
<button
|
||||
ref={signinBtn}
|
||||
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={() => handleSigninBtnsClick("signin")}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="page-content-container">
|
||||
<div className="form-item-container input-form-container">
|
||||
<span className={"normal-text " + (email === "" ? "" : "not-null")}>Email</span>
|
||||
<input type="email" value={email} onChange={handleEmailInputChanged} />
|
||||
</div>
|
||||
<div className="form-item-container input-form-container">
|
||||
<span className={"normal-text " + (password === "" ? "" : "not-null")}>Password</span>
|
||||
<input type="password" value={password} onChange={handlePasswordInputChanged} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="action-btns-container">
|
||||
<button className={`btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`} onClick={handleAutoSigninAsGuestBtnClick}>
|
||||
Login as Guest
|
||||
</button>
|
||||
<span className="split-text">/</span>
|
||||
{siteOwner || pageLoadingState.isLoading ? (
|
||||
<button
|
||||
className={`btn signin-btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={() => handleSigninBtnsClick()}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className={`btn signin-btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={() => handleSignUpAsOwnerBtnsClick()}
|
||||
>
|
||||
Sign up as Owner
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="tip-text">
|
||||
{siteOwner || pageLoadingState.isLoading
|
||||
? "If you don't have an account, please contact the site owner or login as guest."
|
||||
: "You are registering as the site owner."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -32,22 +32,12 @@ class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
public async checkUsernameUsable(username: string): Promise<boolean> {
|
||||
const isUsable = await api.checkUsernameUsable(username);
|
||||
return isUsable;
|
||||
}
|
||||
|
||||
public async updateUsername(name: string): Promise<void> {
|
||||
await api.updateUserinfo({
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
public async checkPasswordValid(password: string): Promise<boolean> {
|
||||
const isValid = await api.checkPasswordValid(password);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
public async updatePassword(password: string): Promise<void> {
|
||||
await api.updateUserinfo({
|
||||
password,
|
||||
|
6
web/src/types/api.d.ts
vendored
6
web/src/types/api.d.ts
vendored
@@ -1 +1,5 @@
|
||||
declare namespace Api {}
|
||||
declare namespace API {
|
||||
interface SystemStatus {
|
||||
owner: Model.User;
|
||||
}
|
||||
}
|
||||
|
4
web/src/types/models.d.ts
vendored
4
web/src/types/models.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
type UserRole = "OWNER" | "USER";
|
||||
|
||||
declare namespace Model {
|
||||
interface BaseModel {
|
||||
id: string;
|
||||
@@ -9,6 +11,8 @@ declare namespace Model {
|
||||
}
|
||||
|
||||
interface User extends BaseModel {
|
||||
role: UserRole;
|
||||
email: string;
|
||||
name: string;
|
||||
openId: string;
|
||||
}
|
||||
|
Reference in New Issue
Block a user