diff --git a/api/auth.go b/api/auth.go index 504c8c0b..aa1a6aef 100644 --- a/api/auth.go +++ b/api/auth.go @@ -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"` } diff --git a/api/system.go b/api/system.go new file mode 100644 index 00000000..5f7c96fe --- /dev/null +++ b/api/system.go @@ -0,0 +1,5 @@ +package api + +type SystemStatus struct { + Owner *User `json:"owner"` +} diff --git a/api/user.go b/api/user.go index c7d0fbe5..364a0e88 100644 --- a/api/user.go +++ b/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) diff --git a/resources/memos_prod.db b/resources/memos_prod.db deleted file mode 100644 index 62a1ae29..00000000 Binary files a/resources/memos_prod.db and /dev/null differ diff --git a/server/auth.go b/server/auth.go index ea904b7a..4cbd0987 100644 --- a/server/auth.go +++ b/server/auth.go @@ -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(), diff --git a/server/basic_auth.go b/server/basic_auth.go index 1c972d5c..574988c6 100644 --- a/server/basic_auth.go +++ b/server/basic_auth.go @@ -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) } diff --git a/server/system.go b/server/system.go index 92afc219..3c0ca7a7 100644 --- a/server/system.go +++ b/server/system.go @@ -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 + }) } diff --git a/server/user.go b/server/user.go index 195913c8..46242d1f 100644 --- a/server/user.go +++ b/server/user.go @@ -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) diff --git a/store/migration/10001__schema.sql b/store/migration/10001__schema.sql index bf07cc5a..0823e7da 100644 --- a/store/migration/10001__schema.sql +++ b/store/migration/10001__schema.sql @@ -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 diff --git a/store/seed/10001__user.sql b/store/seed/10001__user.sql index 2d89d17f..3a3a5495 100644 --- a/store/seed/10001__user.sql +++ b/store/seed/10001__user.sql @@ -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' diff --git a/store/sqlite.go b/store/sqlite.go index 28ad935b..f04c1263 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -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 { diff --git a/store/user.go b/store/user.go index 71182cb6..6727ab5a 100644 --- a/store/user.go +++ b/store/user.go @@ -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, diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index 8823a039..8df6437e 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -15,7 +15,6 @@ const validateConfig: ValidatorConfig = { interface Props extends DialogProps {} const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { - const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState(""); @@ -27,11 +26,6 @@ const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { destroy(); }; - const handleOldPasswordChanged = (e: React.ChangeEvent) => { - const text = e.target.value as string; - setOldPassword(text); - }; - const handleNewPasswordChanged = (e: React.ChangeEvent) => { const text = e.target.value as string; setNewPassword(text); @@ -43,7 +37,7 @@ const ChangePasswordDialog: React.FC = ({ 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 = ({ 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 = ({ destroy }: Props) => {
-
diff --git a/web/src/components/MyAccountSection.tsx b/web/src/components/MyAccountSection.tsx index f0603ee6..ce2d18d2 100644 --- a/web/src/components/MyAccountSection.tsx +++ b/web/src/components/MyAccountSection.tsx @@ -40,13 +40,6 @@ const MyAccountSection: React.FC = () => { } 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 = () => { Created at: {utils.getDateString(user.createdAt)} +
- {showAutoSigninAsGuest ? ( - <> -
-
- 👉 Quick login as a guest -
-
- Sign in/up with account -
-
- - ) : ( - <> -
-
- Username - -
-
- Password - -
-
-
-
{/* nth */}
-
- - / - - / - -
-
- - )} +
+
+ Email + +
+
+ Password + +
+
+
+ + / + {siteOwner || pageLoadingState.isLoading ? ( + + ) : ( + + )} +
+

+ {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."} +

); diff --git a/web/src/services/userService.ts b/web/src/services/userService.ts index d6a2ffd6..a9616c37 100644 --- a/web/src/services/userService.ts +++ b/web/src/services/userService.ts @@ -32,22 +32,12 @@ class UserService { }); } - public async checkUsernameUsable(username: string): Promise { - const isUsable = await api.checkUsernameUsable(username); - return isUsable; - } - public async updateUsername(name: string): Promise { await api.updateUserinfo({ name, }); } - public async checkPasswordValid(password: string): Promise { - const isValid = await api.checkPasswordValid(password); - return isValid; - } - public async updatePassword(password: string): Promise { await api.updateUserinfo({ password, diff --git a/web/src/types/api.d.ts b/web/src/types/api.d.ts index 92044c03..10884c05 100644 --- a/web/src/types/api.d.ts +++ b/web/src/types/api.d.ts @@ -1 +1,5 @@ -declare namespace Api {} +declare namespace API { + interface SystemStatus { + owner: Model.User; + } +} diff --git a/web/src/types/models.d.ts b/web/src/types/models.d.ts index 38a3aadf..fef2bb74 100644 --- a/web/src/types/models.d.ts +++ b/web/src/types/models.d.ts @@ -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; }