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

@ -1,11 +1,13 @@
package api package api
type Login struct { type Login struct {
Name string `json:"name"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
} }
type Signup struct { type Signup struct {
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"` Name string `json:"name"`
Password string `json:"password"` Password string `json:"password"`
} }

5
api/system.go Normal file
View File

@ -0,0 +1,5 @@
package api
type SystemStatus struct {
Owner *User `json:"owner"`
}

View File

@ -1,5 +1,15 @@
package api package api
// Role is the type of a role.
type Role string
const (
// Owner is the OWNER role.
Owner Role = "OWNER"
// NormalUser is the USER role.
NormalUser Role = "USER"
)
type User struct { type User struct {
ID int `json:"id"` ID int `json:"id"`
@ -8,45 +18,44 @@ type User struct {
UpdatedTs int64 `json:"updatedTs"` UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields // Domain specific fields
OpenID string `json:"openId"` Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"` Name string `json:"name"`
PasswordHash string `json:"-"` PasswordHash string `json:"-"`
OpenID string `json:"openId"`
} }
type UserCreate struct { type UserCreate struct {
// Domain specific fields // Domain specific fields
OpenID string Email string
Role Role
Name string Name string
PasswordHash string PasswordHash string
OpenID string
} }
type UserPatch struct { type UserPatch struct {
ID int ID int
// Domain specific fields // Domain specific fields
OpenID *string Email *string `json:"email"`
PasswordHash *string
Name *string `json:"name"` Name *string `json:"name"`
Password *string `json:"password"` Password *string `json:"password"`
ResetOpenID *bool `json:"resetOpenId"` ResetOpenID *bool `json:"resetOpenId"`
PasswordHash *string
OpenID *string
} }
type UserFind struct { type UserFind struct {
ID *int `json:"id"` ID *int `json:"id"`
// Domain specific fields // Domain specific fields
Email *string `json:"email"`
Role *Role
Name *string `json:"name"` Name *string `json:"name"`
OpenID *string OpenID *string
} }
type UserRenameCheck struct {
Name string `json:"name"`
}
type UserPasswordCheck struct {
Password string `json:"password"`
}
type UserService interface { type UserService interface {
CreateUser(create *UserCreate) (*User, error) CreateUser(create *UserCreate) (*User, error)
PatchUser(patch *UserPatch) (*User, error) PatchUser(patch *UserPatch) (*User, error)

Binary file not shown.

View File

@ -19,14 +19,14 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
} }
userFind := &api.UserFind{ userFind := &api.UserFind{
Name: &login.Name, Email: &login.Email,
} }
user, err := s.UserService.FindUser(userFind) user, err := s.UserService.FindUser(userFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by name %s", login.Name)).SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", login.Email)).SetInternal(err)
} }
if user == nil { if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with name %s", login.Name)) return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", login.Email))
} }
// Compare the stored hashed password, with the hashed version of the password that was received. // Compare the stored hashed password, with the hashed version of the password that was received.
@ -58,6 +58,19 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
}) })
g.POST("/auth/signup", func(c echo.Context) error { g.POST("/auth/signup", func(c echo.Context) error {
// Don't allow to signup by this api if site owner existed.
ownerUserType := api.Owner
ownerUserFind := api.UserFind{
Role: &ownerUserType,
}
ownerUser, err := s.UserService.FindUser(&ownerUserFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find owner user").SetInternal(err)
}
if ownerUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Owner existed, please contact the site owner to signin account firstly.").SetInternal(err)
}
signup := &api.Signup{} signup := &api.Signup{}
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil { if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
@ -65,6 +78,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
// Validate signup form. // Validate signup form.
// We can do stricter checks later. // We can do stricter checks later.
if len(signup.Email) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 6.")
}
if len(signup.Name) < 6 { if len(signup.Name) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Username is too short, minimum length is 6.") return echo.NewHTTPError(http.StatusBadRequest, "Username is too short, minimum length is 6.")
} }
@ -73,14 +89,14 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
} }
userFind := &api.UserFind{ userFind := &api.UserFind{
Name: &signup.Name, Email: &signup.Email,
} }
user, err := s.UserService.FindUser(userFind) user, err := s.UserService.FindUser(userFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by name %s", signup.Name)).SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signup.Email)).SetInternal(err)
} }
if user != nil { if user != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Existed user found: %s", signup.Name)) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Existed user found: %s", signup.Email))
} }
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost) passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
@ -89,6 +105,8 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
} }
userCreate := &api.UserCreate{ userCreate := &api.UserCreate{
Email: signup.Email,
Role: api.Role(signup.Role),
Name: signup.Name, Name: signup.Name,
PasswordHash: string(passwordHash), PasswordHash: string(passwordHash),
OpenID: common.GenUUID(), OpenID: common.GenUUID(),

View File

@ -56,7 +56,7 @@ func removeUserSession(c echo.Context) error {
func BasicAuthMiddleware(us api.UserService, next echo.HandlerFunc) echo.HandlerFunc { func BasicAuthMiddleware(us api.UserService, next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
// Skips auth // Skips auth
if common.HasPrefixes(c.Path(), "/api/auth", "/api/ping") { if common.HasPrefixes(c.Path(), "/api/auth", "/api/ping", "/api/status") {
return next(c) return next(c)
} }

View File

@ -2,6 +2,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"memos/api"
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -16,4 +17,26 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
return nil return nil
}) })
g.GET("/status", func(c echo.Context) error {
ownerUserType := api.Owner
ownerUserFind := api.UserFind{
Role: &ownerUserType,
}
ownerUser, err := s.UserService.FindUser(&ownerUserFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find owner user").SetInternal(err)
}
systemStatus := api.SystemStatus{
Owner: ownerUser,
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemStatus)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system status response").SetInternal(err)
}
return nil
})
} }

View File

@ -36,70 +36,6 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil return nil
}) })
g.POST("/user/rename_check", func(c echo.Context) error {
userRenameCheck := &api.UserRenameCheck{}
if err := json.NewDecoder(c.Request().Body).Decode(userRenameCheck); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user rename check request").SetInternal(err)
}
if userRenameCheck.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "New name needed")
}
userFind := &api.UserFind{
Name: &userRenameCheck.Name,
}
user, err := s.UserService.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by name %s", *userFind.Name)).SetInternal(err)
}
isUsable := true
if user != nil {
isUsable = false
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(isUsable)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode rename check response").SetInternal(err)
}
return nil
})
g.POST("/user/password_check", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
userPasswordCheck := &api.UserPasswordCheck{}
if err := json.NewDecoder(c.Request().Body).Decode(userPasswordCheck); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user password check request").SetInternal(err)
}
if userPasswordCheck.Password == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Password needed")
}
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.UserService.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by password").SetInternal(err)
}
isValid := false
// Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(userPasswordCheck.Password)); err == nil {
isValid = true
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(isValid)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode password check response").SetInternal(err)
}
return nil
})
g.PATCH("/user/me", func(c echo.Context) error { g.PATCH("/user/me", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int) userID := c.Get(getUserIDContextKey()).(int)
userPatch := &api.UserPatch{ userPatch := &api.UserPatch{
@ -109,9 +45,17 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
} }
if userPatch.ResetOpenID != nil && *userPatch.ResetOpenID { if userPatch.Email != nil {
openID := common.GenUUID() userFind := api.UserFind{
userPatch.OpenID = &openID Email: userPatch.Email,
}
user, err := s.UserService.FindUser(&userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", *userPatch.Email)).SetInternal(err)
}
if user != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("User with email %s existed", *userPatch.Email)).SetInternal(err)
}
} }
if userPatch.Password != nil && *userPatch.Password != "" { if userPatch.Password != nil && *userPatch.Password != "" {
@ -124,6 +68,11 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
userPatch.PasswordHash = &passwordHashStr userPatch.PasswordHash = &passwordHashStr
} }
if userPatch.ResetOpenID != nil && *userPatch.ResetOpenID {
openID := common.GenUUID()
userPatch.OpenID = &openID
}
user, err := s.UserService.PatchUser(userPatch) user, err := s.UserService.PatchUser(userPatch)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)

View File

@ -1,12 +1,14 @@
-- user -- user
CREATE TABLE user ( CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('OWNER', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL, name TEXT NOT NULL,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
open_id TEXT NOT NULL, open_id TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(`name`, `open_id`) UNIQUE(`email`, `open_id`)
); );
INSERT INTO INSERT INTO

View File

@ -2,6 +2,7 @@ INSERT INTO
user ( user (
`id`, `id`,
`name`, `name`,
`email`,
`open_id`, `open_id`,
`password_hash` `password_hash`
) )
@ -9,22 +10,7 @@ VALUES
( (
101, 101,
'guest', 'guest',
'guest_open_id', 'guest@example.com',
-- "secret"
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
INSERT INTO
user (
`id`,
`name`,
`open_id`,
`password_hash`
)
VALUES
(
102,
'dear_musk',
'guest_open_id', 'guest_open_id',
-- "secret" -- "secret"
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK' '$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'

View File

@ -49,7 +49,6 @@ func (db *DB) Open() (err error) {
} }
db.Db = sqlDB db.Db = sqlDB
// If db file not exists, we should migrate and seed the database. // If db file not exists, we should migrate and seed the database.
if _, err := os.Stat(db.DSN); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(db.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.migrate(); err != nil { if err := db.migrate(); err != nil {

View File

@ -51,13 +51,17 @@ func (s *UserService) FindUser(find *api.UserFind) (*api.User, error) {
func createUser(db *DB, create *api.UserCreate) (*api.User, error) { func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
row, err := db.Db.Query(` row, err := db.Db.Query(`
INSERT INTO user ( INSERT INTO user (
email,
role,
name, name,
password_hash, password_hash,
open_id open_id
) )
VALUES (?, ?, ?) VALUES (?, ?, ?, ?, ?)
RETURNING id, name, password_hash, open_id, created_ts, updated_ts RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts
`, `,
create.Email,
create.Role,
create.Name, create.Name,
create.PasswordHash, create.PasswordHash,
create.OpenID, create.OpenID,
@ -71,6 +75,8 @@ func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
var user api.User var user api.User
if err := row.Scan( if err := row.Scan(
&user.ID, &user.ID,
&user.Email,
&user.Role,
&user.Name, &user.Name,
&user.PasswordHash, &user.PasswordHash,
&user.OpenID, &user.OpenID,
@ -86,6 +92,9 @@ func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) { func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
set, args := []string{}, []interface{}{} set, args := []string{}, []interface{}{}
if v := patch.Email; v != nil {
set, args = append(set, "email = ?"), append(args, v)
}
if v := patch.Name; v != nil { if v := patch.Name; v != nil {
set, args = append(set, "name = ?"), append(args, v) set, args = append(set, "name = ?"), append(args, v)
} }
@ -102,7 +111,7 @@ func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
UPDATE user UPDATE user
SET `+strings.Join(set, ", ")+` SET `+strings.Join(set, ", ")+`
WHERE id = ? WHERE id = ?
RETURNING id, name, password_hash, open_id, created_ts, updated_ts RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts
`, args...) `, args...)
if err != nil { if err != nil {
return nil, FormatError(err) return nil, FormatError(err)
@ -113,6 +122,8 @@ func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
var user api.User var user api.User
if err := row.Scan( if err := row.Scan(
&user.ID, &user.ID,
&user.Email,
&user.Role,
&user.Name, &user.Name,
&user.PasswordHash, &user.PasswordHash,
&user.OpenID, &user.OpenID,
@ -134,6 +145,12 @@ func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
if v := find.ID; v != nil { if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v) where, args = append(where, "id = ?"), append(args, *v)
} }
if v := find.Role; v != nil {
where, args = append(where, "role = ?"), append(args, *v)
}
if v := find.Email; v != nil {
where, args = append(where, "email = ?"), append(args, *v)
}
if v := find.Name; v != nil { if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, *v) where, args = append(where, "name = ?"), append(args, *v)
} }
@ -144,6 +161,8 @@ func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
rows, err := db.Db.Query(` rows, err := db.Db.Query(`
SELECT SELECT
id, id,
email,
role,
name, name,
password_hash, password_hash,
open_id, open_id,
@ -163,6 +182,8 @@ func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
var user api.User var user api.User
if err := rows.Scan( if err := rows.Scan(
&user.ID, &user.ID,
&user.Email,
&user.Role,
&user.Name, &user.Name,
&user.PasswordHash, &user.PasswordHash,
&user.OpenID, &user.OpenID,

View File

@ -15,7 +15,6 @@ const validateConfig: ValidatorConfig = {
interface Props extends DialogProps {} interface Props extends DialogProps {}
const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => { const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState("");
@ -27,11 +26,6 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
destroy(); destroy();
}; };
const handleOldPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string;
setOldPassword(text);
};
const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string; const text = e.target.value as string;
setNewPassword(text); setNewPassword(text);
@ -43,7 +37,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
}; };
const handleSaveBtnClick = async () => { const handleSaveBtnClick = async () => {
if (oldPassword === "" || newPassword === "" || newPasswordAgain === "") { if (newPassword === "" || newPasswordAgain === "") {
toastHelper.error("Please fill in all fields."); toastHelper.error("Please fill in all fields.");
return; return;
} }
@ -61,14 +55,6 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
} }
try { try {
const isValid = await userService.checkPasswordValid(oldPassword);
if (!isValid) {
toastHelper.error("Old password is invalid.");
setOldPassword("");
return;
}
await userService.updatePassword(newPassword); await userService.updatePassword(newPassword);
toastHelper.info("Password changed."); toastHelper.info("Password changed.");
handleCloseBtnClick(); handleCloseBtnClick();
@ -86,16 +72,12 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
</button> </button>
</div> </div>
<div className="dialog-content-container"> <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"> <label className="form-label input-form-label">
<span className={"normal-text " + (newPassword === "" ? "" : "not-null")}>New passworld</span> <span className={"normal-text " + (newPassword === "" ? "" : "not-null")}>New passworld</span>
<input type="password" value={newPassword} onChange={handleNewPasswordChanged} /> <input type="password" value={newPassword} onChange={handleNewPasswordChanged} />
</label> </label>
<label className="form-label input-form-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} /> <input type="password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
</label> </label>
<div className="btns-container"> <div className="btns-container">

View File

@ -40,13 +40,6 @@ const MyAccountSection: React.FC<Props> = () => {
} }
try { try {
const isUsable = await userService.checkUsernameUsable(username);
if (!isUsable) {
toastHelper.error("Username is not available");
return;
}
await userService.updateUsername(username); await userService.updateUsername(username);
await userService.doSignIn(); await userService.doSignIn();
toastHelper.info("Username changed"); toastHelper.info("Username changed");
@ -80,6 +73,10 @@ const MyAccountSection: React.FC<Props> = () => {
<span className="normal-text">Created at:</span> <span className="normal-text">Created at:</span>
<span className="normal-text">{utils.getDateString(user.createdAt)}</span> <span className="normal-text">{utils.getDateString(user.createdAt)}</span>
</label> </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"> <label className="form-label input-form-label username-label">
<span className="normal-text">Username:</span> <span className="normal-text">Username:</span>
<input type="text" value={username} onChange={handleUsernameChanged} /> <input type="text" value={username} onChange={handleUsernameChanged} />

View File

@ -39,6 +39,13 @@ async function request<T>(config: RequestConfig): Promise<T> {
} }
namespace api { namespace api {
export function getSystemStatus() {
return request<API.SystemStatus>({
method: "GET",
url: "/api/status",
});
}
export function getUserInfo() { export function getUserInfo() {
return request<Model.User>({ return request<Model.User>({
method: "GET", 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>({ return request<Model.User>({
method: "POST", method: "POST",
url: "/api/auth/login", url: "/api/auth/login",
data: { data: {
name, email,
password, password,
}, },
}); });
} }
export function signup(name: string, password: string) { export function signup(email: string, role: UserRole, name: string, password: string) {
return request<Model.User>({ return request<Model.User>({
method: "POST", method: "POST",
url: "/api/auth/signup", url: "/api/auth/signup",
data: { data: {
email,
role,
name, name,
password, password,
}, },

View File

@ -18,11 +18,11 @@
> .page-content-container { > .page-content-container {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
@apply flex-nowrap; @apply w-full;
> .form-item-container { > .form-item-container {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
@apply relative w-full text-base m-2; @apply relative w-full text-base my-2;
> .normal-text { > .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; @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,12 +46,9 @@
} }
} }
> .page-footer-container { > .action-btns-container {
.flex(row, space-between, center); .flex(row, flex-end, center);
@apply w-full mt-3; @apply w-full mt-2;
> .btns-container {
.flex(row, flex-start, center);
> .btn { > .btn {
@apply px-1 py-2 text-sm rounded; @apply px-1 py-2 text-sm rounded;
@ -81,27 +78,9 @@
@apply text-gray-400 mx-2; @apply text-gray-400 mx-2;
} }
} }
}
> .quickly-btns-container { > .tip-text {
.flex(column, flex-start, flex-start); @apply w-full text-sm mt-4 text-gray-500 text-right;
@apply w-full mt-6;
> .btn {
@apply mb-6 text-base leading-10 border border-solid border-gray-400 px-4 rounded-3xl;
&:hover {
@apply opacity-80;
}
&.guest-signin {
@apply text-green-600 border-2 border-green-600 font-bold;
}
&.requesting {
@apply cursor-wait opacity-80;
}
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import api from "../helpers/api"; import api from "../helpers/api";
import { validate, ValidatorConfig } from "../helpers/validator"; import { validate, ValidatorConfig } from "../helpers/validator";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
@ -16,31 +16,22 @@ const validateConfig: ValidatorConfig = {
}; };
const Signin: React.FC<Props> = () => { 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 [password, setPassword] = useState("");
const [showAutoSigninAsGuest, setShowAutoSigninAsGuest] = useState(true); const actionBtnLoadingState = useLoading(false);
const signinBtnsClickLoadingState = useLoading(false);
const autoSigninAsGuestBtn = useRef<HTMLDivElement>(null);
const signinBtn = useRef<HTMLButtonElement>(null);
useEffect(() => { useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => { api.getSystemStatus().then((status) => {
if (e.key === "Enter") { setSiteOwner(status.owner);
autoSigninAsGuestBtn.current?.click(); pageLoadingState.setFinish();
signinBtn.current?.click(); });
}
};
document.body.addEventListener("keypress", handleKeyPress);
return () => {
document.body.removeEventListener("keypress", handleKeyPress);
};
}, []); }, []);
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string; const text = e.target.value as string;
setUsername(text); setEmail(text);
}; };
const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -48,14 +39,14 @@ const Signin: React.FC<Props> = () => {
setPassword(text); setPassword(text);
}; };
const handleSigninBtnsClick = async (action: "signin" | "signup" = "signin") => { const handleSigninBtnsClick = async () => {
if (signinBtnsClickLoadingState.isLoading) { if (actionBtnLoadingState.isLoading) {
return; return;
} }
const usernameValidResult = validate(username, validateConfig); const emailValidResult = validate(email, validateConfig);
if (!usernameValidResult.result) { if (!emailValidResult.result) {
toastHelper.error("Username: " + usernameValidResult.reason); toastHelper.error("Email: " + emailValidResult.reason);
return; return;
} }
@ -66,13 +57,8 @@ const Signin: React.FC<Props> = () => {
} }
try { try {
signinBtnsClickLoadingState.setLoading(); actionBtnLoadingState.setLoading();
let actionFunc = api.login; await api.login(email, password);
if (action === "signup") {
actionFunc = api.signup;
}
await actionFunc(username, password);
const user = await userService.doSignIn(); const user = await userService.doSignIn();
if (user) { if (user) {
locationService.replaceHistory("/"); locationService.replaceHistory("/");
@ -83,25 +69,52 @@ const Signin: React.FC<Props> = () => {
console.error(error); console.error(error);
toastHelper.error("😟 " + error.message); toastHelper.error("😟 " + error.message);
} }
signinBtnsClickLoadingState.setFinish(); actionBtnLoadingState.setFinish();
}; };
const handleSwitchAccountSigninBtnClick = () => { const handleSignUpAsOwnerBtnsClick = async () => {
if (signinBtnsClickLoadingState.isLoading) { if (actionBtnLoadingState.isLoading) {
return; 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 () => { const handleAutoSigninAsGuestBtnClick = async () => {
if (signinBtnsClickLoadingState.isLoading) { if (actionBtnLoadingState.isLoading) {
return; return;
} }
try { try {
signinBtnsClickLoadingState.setLoading(); actionBtnLoadingState.setLoading();
await api.login("guest", "secret"); await api.login("guest@example.com", "secret");
const user = await userService.doSignIn(); const user = await userService.doSignIn();
if (user) { if (user) {
@ -113,7 +126,7 @@ const Signin: React.FC<Props> = () => {
console.error(error); console.error(error);
toastHelper.error("😟 " + error.message); toastHelper.error("😟 " + error.message);
} }
signinBtnsClickLoadingState.setFinish(); actionBtnLoadingState.setFinish();
}; };
return ( return (
@ -124,65 +137,43 @@ const Signin: React.FC<Props> = () => {
<span className="icon-text"></span> Memos <span className="icon-text"></span> Memos
</p> </p>
</div> </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="page-content-container">
<div className="form-item-container input-form-container"> <div className="form-item-container input-form-container">
<span className={"normal-text " + (username === "" ? "" : "not-null")}>Username</span> <span className={"normal-text " + (email === "" ? "" : "not-null")}>Email</span>
<input type="text" autoComplete="off" value={username} onChange={handleUsernameInputChanged} /> <input type="email" value={email} onChange={handleEmailInputChanged} />
</div> </div>
<div className="form-item-container input-form-container"> <div className="form-item-container input-form-container">
<span className={"normal-text " + (password === "" ? "" : "not-null")}>Password</span> <span className={"normal-text " + (password === "" ? "" : "not-null")}>Password</span>
<input type="password" autoComplete="off" value={password} onChange={handlePasswordInputChanged} /> <input type="password" value={password} onChange={handlePasswordInputChanged} />
</div> </div>
</div> </div>
<div className="page-footer-container"> <div className="action-btns-container">
<div className="btns-container">{/* nth */}</div> <button className={`btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`} onClick={handleAutoSigninAsGuestBtnClick}>
<div className="btns-container">
<button
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
onClick={handleAutoSigninAsGuestBtnClick}
>
Login as Guest Login as Guest
</button> </button>
<span className="split-text">/</span> <span className="split-text">/</span>
{siteOwner || pageLoadingState.isLoading ? (
<button <button
className={`btn signup-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`} className={`btn signin-btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}
onClick={() => handleSigninBtnsClick("signup")} onClick={() => handleSigninBtnsClick()}
>
Sign up
</button>
<span className="split-text">/</span>
<button
ref={signinBtn}
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
onClick={() => handleSigninBtnsClick("signin")}
> >
Sign in Sign in
</button> </button>
</div> ) : (
</div> <button
</> className={`btn signin-btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}
onClick={() => handleSignUpAsOwnerBtnsClick()}
>
Sign up as Owner
</button>
)} )}
</div> </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> </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> { public async updateUsername(name: string): Promise<void> {
await api.updateUserinfo({ await api.updateUserinfo({
name, name,
}); });
} }
public async checkPasswordValid(password: string): Promise<boolean> {
const isValid = await api.checkPasswordValid(password);
return isValid;
}
public async updatePassword(password: string): Promise<void> { public async updatePassword(password: string): Promise<void> {
await api.updateUserinfo({ await api.updateUserinfo({
password, 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 { declare namespace Model {
interface BaseModel { interface BaseModel {
id: string; id: string;
@ -9,6 +11,8 @@ declare namespace Model {
} }
interface User extends BaseModel { interface User extends BaseModel {
role: UserRole;
email: string;
name: string; name: string;
openId: string; openId: string;
} }