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
|
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
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
|
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.
@ -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(),
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
|
@ -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} />
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
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 {
|
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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user