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:
@ -1,11 +1,13 @@
|
||||
package api
|
||||
|
||||
type Login struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type Signup struct {
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
5
api/system.go
Normal file
5
api/system.go
Normal file
@ -0,0 +1,5 @@
|
||||
package api
|
||||
|
||||
type SystemStatus struct {
|
||||
Owner *User `json:"owner"`
|
||||
}
|
33
api/user.go
33
api/user.go
@ -1,5 +1,15 @@
|
||||
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 {
|
||||
ID int `json:"id"`
|
||||
|
||||
@ -8,45 +18,44 @@ type User struct {
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
|
||||
// Domain specific fields
|
||||
OpenID string `json:"openId"`
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
PasswordHash string `json:"-"`
|
||||
OpenID string `json:"openId"`
|
||||
}
|
||||
|
||||
type UserCreate struct {
|
||||
// Domain specific fields
|
||||
OpenID string
|
||||
Email string
|
||||
Role Role
|
||||
Name string
|
||||
PasswordHash string
|
||||
OpenID string
|
||||
}
|
||||
|
||||
type UserPatch struct {
|
||||
ID int
|
||||
|
||||
// Domain specific fields
|
||||
OpenID *string
|
||||
PasswordHash *string
|
||||
Email *string `json:"email"`
|
||||
Name *string `json:"name"`
|
||||
Password *string `json:"password"`
|
||||
ResetOpenID *bool `json:"resetOpenId"`
|
||||
PasswordHash *string
|
||||
OpenID *string
|
||||
}
|
||||
|
||||
type UserFind struct {
|
||||
ID *int `json:"id"`
|
||||
|
||||
// Domain specific fields
|
||||
Email *string `json:"email"`
|
||||
Role *Role
|
||||
Name *string `json:"name"`
|
||||
OpenID *string
|
||||
}
|
||||
|
||||
type UserRenameCheck struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type UserPasswordCheck struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type UserService interface {
|
||||
CreateUser(create *UserCreate) (*User, error)
|
||||
PatchUser(patch *UserPatch) (*User, error)
|
||||
|
Binary file not shown.
@ -19,14 +19,14 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
userFind := &api.UserFind{
|
||||
Name: &login.Name,
|
||||
Email: &login.Email,
|
||||
}
|
||||
user, err := s.UserService.FindUser(userFind)
|
||||
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 {
|
||||
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.
|
||||
@ -58,6 +58,19 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
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{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
|
||||
@ -65,6 +78,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
|
||||
// Validate signup form.
|
||||
// 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 {
|
||||
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{
|
||||
Name: &signup.Name,
|
||||
Email: &signup.Email,
|
||||
}
|
||||
user, err := s.UserService.FindUser(userFind)
|
||||
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 {
|
||||
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)
|
||||
@ -89,6 +105,8 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
userCreate := &api.UserCreate{
|
||||
Email: signup.Email,
|
||||
Role: api.Role(signup.Role),
|
||||
Name: signup.Name,
|
||||
PasswordHash: string(passwordHash),
|
||||
OpenID: common.GenUUID(),
|
||||
|
@ -56,7 +56,7 @@ func removeUserSession(c echo.Context) error {
|
||||
func BasicAuthMiddleware(us api.UserService, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"memos/api"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -16,4 +17,26 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -36,70 +36,6 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
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 {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
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)
|
||||
}
|
||||
|
||||
if userPatch.ResetOpenID != nil && *userPatch.ResetOpenID {
|
||||
openID := common.GenUUID()
|
||||
userPatch.OpenID = &openID
|
||||
if userPatch.Email != nil {
|
||||
userFind := api.UserFind{
|
||||
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 != "" {
|
||||
@ -124,6 +68,11 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
userPatch.PasswordHash = &passwordHashStr
|
||||
}
|
||||
|
||||
if userPatch.ResetOpenID != nil && *userPatch.ResetOpenID {
|
||||
openID := common.GenUUID()
|
||||
userPatch.OpenID = &openID
|
||||
}
|
||||
|
||||
user, err := s.UserService.PatchUser(userPatch)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
|
||||
|
@ -1,12 +1,14 @@
|
||||
-- user
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('OWNER', 'USER')) DEFAULT 'USER',
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
open_id TEXT NOT NULL,
|
||||
created_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
|
||||
|
@ -2,6 +2,7 @@ INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`name`,
|
||||
`email`,
|
||||
`open_id`,
|
||||
`password_hash`
|
||||
)
|
||||
@ -9,22 +10,7 @@ VALUES
|
||||
(
|
||||
101,
|
||||
'guest',
|
||||
'guest_open_id',
|
||||
-- "secret"
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`name`,
|
||||
`open_id`,
|
||||
`password_hash`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
102,
|
||||
'dear_musk',
|
||||
'guest@example.com',
|
||||
'guest_open_id',
|
||||
-- "secret"
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||
|
@ -49,7 +49,6 @@ func (db *DB) Open() (err error) {
|
||||
}
|
||||
|
||||
db.Db = sqlDB
|
||||
|
||||
// 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 := db.migrate(); err != nil {
|
||||
|
@ -51,13 +51,17 @@ func (s *UserService) FindUser(find *api.UserFind) (*api.User, error) {
|
||||
func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
|
||||
row, err := db.Db.Query(`
|
||||
INSERT INTO user (
|
||||
email,
|
||||
role,
|
||||
name,
|
||||
password_hash,
|
||||
open_id
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id, name, password_hash, open_id, created_ts, updated_ts
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts
|
||||
`,
|
||||
create.Email,
|
||||
create.Role,
|
||||
create.Name,
|
||||
create.PasswordHash,
|
||||
create.OpenID,
|
||||
@ -71,6 +75,8 @@ func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
|
||||
var user api.User
|
||||
if err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.Name,
|
||||
&user.PasswordHash,
|
||||
&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) {
|
||||
set, args := []string{}, []interface{}{}
|
||||
|
||||
if v := patch.Email; v != nil {
|
||||
set, args = append(set, "email = ?"), append(args, v)
|
||||
}
|
||||
if v := patch.Name; v != nil {
|
||||
set, args = append(set, "name = ?"), append(args, v)
|
||||
}
|
||||
@ -102,7 +111,7 @@ func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
|
||||
UPDATE user
|
||||
SET `+strings.Join(set, ", ")+`
|
||||
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...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
@ -113,6 +122,8 @@ func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
|
||||
var user api.User
|
||||
if err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.Name,
|
||||
&user.PasswordHash,
|
||||
&user.OpenID,
|
||||
@ -134,6 +145,12 @@ func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
|
||||
if v := find.ID; v != nil {
|
||||
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 {
|
||||
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(`
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
role,
|
||||
name,
|
||||
password_hash,
|
||||
open_id,
|
||||
@ -163,6 +182,8 @@ func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
|
||||
var user api.User
|
||||
if err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.Name,
|
||||
&user.PasswordHash,
|
||||
&user.OpenID,
|
||||
|
@ -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,12 +46,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
> .page-footer-container {
|
||||
.flex(row, space-between, center);
|
||||
@apply w-full mt-3;
|
||||
|
||||
> .btns-container {
|
||||
.flex(row, flex-start, center);
|
||||
> .action-btns-container {
|
||||
.flex(row, flex-end, center);
|
||||
@apply w-full mt-2;
|
||||
|
||||
> .btn {
|
||||
@apply px-1 py-2 text-sm rounded;
|
||||
@ -81,27 +78,9 @@
|
||||
@apply text-gray-400 mx-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .quickly-btns-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@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;
|
||||
}
|
||||
}
|
||||
> .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,65 +137,43 @@ 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} />
|
||||
<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" autoComplete="off" value={password} onChange={handlePasswordInputChanged} />
|
||||
<input type="password" 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}
|
||||
>
|
||||
<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 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")}
|
||||
className={`btn signin-btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={() => handleSigninBtnsClick()}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<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