feat: add user role field (#49)

* feat: add user role field

* chore: fix typo

* feat: update signup api
This commit is contained in:
STEVEN
2022-05-15 10:57:54 +08:00
committed by GitHub
parent 64374610ea
commit f1cca0f298
20 changed files with 264 additions and 294 deletions

View File

@@ -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">

View File

@@ -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} />

View File

@@ -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,
},

View File

@@ -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;
}
}
}

View File

@@ -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>
);

View File

@@ -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,

View File

@@ -1 +1,5 @@
declare namespace Api {}
declare namespace API {
interface SystemStatus {
owner: Model.User;
}
}

View File

@@ -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;
}