mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: add username field (#544)
* feat: add username field * chore: update
This commit is contained in:
@ -1,13 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
type Signin struct {
|
type Signin struct {
|
||||||
Email string `json:"email"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Signup struct {
|
type Signup struct {
|
||||||
Email string `json:"email"`
|
Username string `json:"username"`
|
||||||
Role Role `json:"role"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
Role Role `json:"role"`
|
||||||
}
|
}
|
||||||
|
39
api/user.go
39
api/user.go
@ -2,8 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/usememos/memos/common"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Role is the type of a role.
|
// Role is the type of a role.
|
||||||
@ -12,6 +10,8 @@ type Role string
|
|||||||
const (
|
const (
|
||||||
// Host is the HOST role.
|
// Host is the HOST role.
|
||||||
Host Role = "HOST"
|
Host Role = "HOST"
|
||||||
|
// Admin is the ADMIN role.
|
||||||
|
Admin Role = "ADMIN"
|
||||||
// NormalUser is the USER role.
|
// NormalUser is the USER role.
|
||||||
NormalUser Role = "USER"
|
NormalUser Role = "USER"
|
||||||
)
|
)
|
||||||
@ -20,6 +20,8 @@ func (e Role) String() string {
|
|||||||
switch e {
|
switch e {
|
||||||
case Host:
|
case Host:
|
||||||
return "HOST"
|
return "HOST"
|
||||||
|
case Admin:
|
||||||
|
return "ADMIN"
|
||||||
case NormalUser:
|
case NormalUser:
|
||||||
return "USER"
|
return "USER"
|
||||||
}
|
}
|
||||||
@ -35,9 +37,10 @@ type User struct {
|
|||||||
UpdatedTs int64 `json:"updatedTs"`
|
UpdatedTs int64 `json:"updatedTs"`
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Email string `json:"email"`
|
Username string `json:"username"`
|
||||||
Role Role `json:"role"`
|
Role Role `json:"role"`
|
||||||
Name string `json:"name"`
|
Email string `json:"email"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
PasswordHash string `json:"-"`
|
PasswordHash string `json:"-"`
|
||||||
OpenID string `json:"openId"`
|
OpenID string `json:"openId"`
|
||||||
UserSettingList []*UserSetting `json:"userSettingList"`
|
UserSettingList []*UserSetting `json:"userSettingList"`
|
||||||
@ -45,23 +48,21 @@ type User struct {
|
|||||||
|
|
||||||
type UserCreate struct {
|
type UserCreate struct {
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Email string `json:"email"`
|
Username string `json:"username"`
|
||||||
Role Role `json:"role"`
|
Role Role `json:"role"`
|
||||||
Name string `json:"name"`
|
Email string `json:"email"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
PasswordHash string
|
PasswordHash string
|
||||||
OpenID string
|
OpenID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (create UserCreate) Validate() error {
|
func (create UserCreate) Validate() error {
|
||||||
if !common.ValidateEmail(create.Email) {
|
if len(create.Username) < 4 {
|
||||||
return fmt.Errorf("invalid email format")
|
return fmt.Errorf("username is too short, minimum length is 4")
|
||||||
}
|
}
|
||||||
if len(create.Email) < 6 {
|
if len(create.Password) < 4 {
|
||||||
return fmt.Errorf("email is too short, minimum length is 6")
|
return fmt.Errorf("password is too short, minimum length is 4")
|
||||||
}
|
|
||||||
if len(create.Password) < 6 {
|
|
||||||
return fmt.Errorf("password is too short, minimum length is 6")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -75,8 +76,9 @@ type UserPatch struct {
|
|||||||
RowStatus *RowStatus `json:"rowStatus"`
|
RowStatus *RowStatus `json:"rowStatus"`
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
|
Username *string `json:"username"`
|
||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
Name *string `json:"name"`
|
Nickname *string `json:"nickname"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
ResetOpenID *bool `json:"resetOpenId"`
|
ResetOpenID *bool `json:"resetOpenId"`
|
||||||
PasswordHash *string
|
PasswordHash *string
|
||||||
@ -90,10 +92,11 @@ type UserFind struct {
|
|||||||
RowStatus *RowStatus `json:"rowStatus"`
|
RowStatus *RowStatus `json:"rowStatus"`
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Email *string `json:"email"`
|
Username *string `json:"username"`
|
||||||
Role *Role
|
Role *Role
|
||||||
Name *string `json:"name"`
|
Email *string `json:"email"`
|
||||||
OpenID *string
|
Nickname *string `json:"nickname"`
|
||||||
|
OpenID *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserDelete struct {
|
type UserDelete struct {
|
||||||
|
@ -94,7 +94,7 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
if user != nil {
|
if user != nil {
|
||||||
if user.RowStatus == api.Archived {
|
if user.RowStatus == api.Archived {
|
||||||
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
|
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", user.Username))
|
||||||
}
|
}
|
||||||
c.Set(getUserIDContextKey(), userID)
|
c.Set(getUserIDContextKey(), userID)
|
||||||
}
|
}
|
||||||
|
@ -22,16 +22,16 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userFind := &api.UserFind{
|
userFind := &api.UserFind{
|
||||||
Email: &signin.Email,
|
Username: &signin.Username,
|
||||||
}
|
}
|
||||||
user, err := s.Store.FindUser(ctx, userFind)
|
user, err := s.Store.FindUser(ctx, userFind)
|
||||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by username %s", signin.Username)).SetInternal(err)
|
||||||
}
|
}
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", signin.Email))
|
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with username %s", signin.Username))
|
||||||
} else if user.RowStatus == api.Archived {
|
} else if user.RowStatus == api.Archived {
|
||||||
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", signin.Email))
|
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -107,9 +107,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userCreate := &api.UserCreate{
|
userCreate := &api.UserCreate{
|
||||||
Email: signup.Email,
|
Username: signup.Username,
|
||||||
Role: api.Role(signup.Role),
|
Role: api.Role(signup.Role),
|
||||||
Name: signup.Name,
|
Nickname: signup.Username,
|
||||||
Password: signup.Password,
|
Password: signup.Password,
|
||||||
OpenID: common.GenUUID(),
|
OpenID: common.GenUUID(),
|
||||||
}
|
}
|
||||||
|
@ -46,14 +46,14 @@ func (s *Server) registerRSSRoutes(g *echo.Group) {
|
|||||||
Title: "Memos",
|
Title: "Memos",
|
||||||
Link: &feeds.Link{Href: baseURL},
|
Link: &feeds.Link{Href: baseURL},
|
||||||
Description: "Memos",
|
Description: "Memos",
|
||||||
Author: &feeds.Author{Name: user.Name},
|
Author: &feeds.Author{Name: user.Username},
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
feed.Items = make([]*feeds.Item, len(memoList))
|
feed.Items = make([]*feeds.Item, len(memoList))
|
||||||
for i, memo := range memoList {
|
for i, memo := range memoList {
|
||||||
feed.Items[i] = &feeds.Item{
|
feed.Items[i] = &feeds.Item{
|
||||||
Title: user.Name + "-memos-" + strconv.Itoa(memo.ID),
|
Title: user.Username + "-memos-" + strconv.Itoa(memo.ID),
|
||||||
Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
|
Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
|
||||||
Description: memo.Content,
|
Description: memo.Content,
|
||||||
Created: time.Unix(memo.CreatedTs, 0),
|
Created: time.Unix(memo.CreatedTs, 0),
|
||||||
|
@ -18,9 +18,10 @@ CREATE TABLE user (
|
|||||||
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')),
|
||||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
email TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
|
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
|
||||||
name TEXT NOT NULL,
|
email TEXT NOT NULL DEFAULT '',
|
||||||
|
nickname TEXT NOT NULL DEFAULT '',
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
open_id TEXT NOT NULL UNIQUE
|
open_id TEXT NOT NULL UNIQUE
|
||||||
);
|
);
|
||||||
|
41
store/db/migration/prod/0.8/01__user_username.sql
Normal file
41
store/db/migration/prod/0.8/01__user_username.sql
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
-- add column username TEXT NOT NULL UNIQUE
|
||||||
|
-- rename column name to nickname
|
||||||
|
-- add role `ADMIN`
|
||||||
|
DROP TABLE IF EXISTS _user_old;
|
||||||
|
|
||||||
|
ALTER TABLE user RENAME TO _user_old;
|
||||||
|
|
||||||
|
-- user
|
||||||
|
CREATE TABLE user (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
|
||||||
|
email TEXT NOT NULL DEFAULT '',
|
||||||
|
nickname TEXT NOT NULL DEFAULT '',
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
open_id TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO user (
|
||||||
|
id, created_ts, updated_ts, row_status,
|
||||||
|
username, role, email, nickname, password_hash,
|
||||||
|
open_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
created_ts,
|
||||||
|
updated_ts,
|
||||||
|
row_status,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
password_hash,
|
||||||
|
open_id
|
||||||
|
FROM
|
||||||
|
_user_old;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS _user_old;
|
@ -1,18 +1,20 @@
|
|||||||
INSERT INTO
|
INSERT INTO
|
||||||
user (
|
user (
|
||||||
`id`,
|
`id`,
|
||||||
`email`,
|
`username`,
|
||||||
`role`,
|
`role`,
|
||||||
`name`,
|
`email`,
|
||||||
|
`nickname`,
|
||||||
`open_id`,
|
`open_id`,
|
||||||
`password_hash`
|
`password_hash`
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
101,
|
101,
|
||||||
'demo@usememos.com',
|
'demohero',
|
||||||
'HOST',
|
'HOST',
|
||||||
'Demo Host',
|
'demo@usememos.com',
|
||||||
|
'Demo Hero',
|
||||||
'demo_open_id',
|
'demo_open_id',
|
||||||
-- raw password: secret
|
-- raw password: secret
|
||||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||||
@ -21,17 +23,19 @@ VALUES
|
|||||||
INSERT INTO
|
INSERT INTO
|
||||||
user (
|
user (
|
||||||
`id`,
|
`id`,
|
||||||
`email`,
|
`username`,
|
||||||
`role`,
|
`role`,
|
||||||
`name`,
|
`email`,
|
||||||
|
`nickname`,
|
||||||
`open_id`,
|
`open_id`,
|
||||||
`password_hash`
|
`password_hash`
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
102,
|
102,
|
||||||
'jack@usememos.com',
|
'jack',
|
||||||
'USER',
|
'USER',
|
||||||
|
'jack@usememos.com',
|
||||||
'Jack',
|
'Jack',
|
||||||
'jack_open_id',
|
'jack_open_id',
|
||||||
-- raw password: secret
|
-- raw password: secret
|
||||||
@ -42,9 +46,10 @@ INSERT INTO
|
|||||||
user (
|
user (
|
||||||
`id`,
|
`id`,
|
||||||
`row_status`,
|
`row_status`,
|
||||||
`email`,
|
`username`,
|
||||||
`role`,
|
`role`,
|
||||||
`name`,
|
`email`,
|
||||||
|
`nickname`,
|
||||||
`open_id`,
|
`open_id`,
|
||||||
`password_hash`
|
`password_hash`
|
||||||
)
|
)
|
||||||
@ -52,8 +57,9 @@ VALUES
|
|||||||
(
|
(
|
||||||
103,
|
103,
|
||||||
'ARCHIVED',
|
'ARCHIVED',
|
||||||
'bob@usememos.com',
|
'bob',
|
||||||
'USER',
|
'USER',
|
||||||
|
'bob@usememos.com',
|
||||||
'Bob',
|
'Bob',
|
||||||
'bob_open_id',
|
'bob_open_id',
|
||||||
-- raw password: secret
|
-- raw password: secret
|
||||||
|
@ -21,9 +21,10 @@ type userRaw struct {
|
|||||||
UpdatedTs int64
|
UpdatedTs int64
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Email string
|
Username string
|
||||||
Role api.Role
|
Role api.Role
|
||||||
Name string
|
Email string
|
||||||
|
Nickname string
|
||||||
PasswordHash string
|
PasswordHash string
|
||||||
OpenID string
|
OpenID string
|
||||||
}
|
}
|
||||||
@ -36,9 +37,10 @@ func (raw *userRaw) toUser() *api.User {
|
|||||||
CreatedTs: raw.CreatedTs,
|
CreatedTs: raw.CreatedTs,
|
||||||
UpdatedTs: raw.UpdatedTs,
|
UpdatedTs: raw.UpdatedTs,
|
||||||
|
|
||||||
Email: raw.Email,
|
Username: raw.Username,
|
||||||
Role: raw.Role,
|
Role: raw.Role,
|
||||||
Name: raw.Name,
|
Email: raw.Email,
|
||||||
|
Nickname: raw.Nickname,
|
||||||
PasswordHash: raw.PasswordHash,
|
PasswordHash: raw.PasswordHash,
|
||||||
OpenID: raw.OpenID,
|
OpenID: raw.OpenID,
|
||||||
}
|
}
|
||||||
@ -194,27 +196,30 @@ func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
|
|||||||
func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) {
|
func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) {
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO user (
|
INSERT INTO user (
|
||||||
email,
|
username,
|
||||||
role,
|
role,
|
||||||
name,
|
email,
|
||||||
|
nickname,
|
||||||
password_hash,
|
password_hash,
|
||||||
open_id
|
open_id
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
|
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
|
||||||
`
|
`
|
||||||
var userRaw userRaw
|
var userRaw userRaw
|
||||||
if err := tx.QueryRowContext(ctx, query,
|
if err := tx.QueryRowContext(ctx, query,
|
||||||
create.Email,
|
create.Username,
|
||||||
create.Role,
|
create.Role,
|
||||||
create.Name,
|
create.Email,
|
||||||
|
create.Nickname,
|
||||||
create.PasswordHash,
|
create.PasswordHash,
|
||||||
create.OpenID,
|
create.OpenID,
|
||||||
).Scan(
|
).Scan(
|
||||||
&userRaw.ID,
|
&userRaw.ID,
|
||||||
&userRaw.Email,
|
&userRaw.Username,
|
||||||
&userRaw.Role,
|
&userRaw.Role,
|
||||||
&userRaw.Name,
|
&userRaw.Email,
|
||||||
|
&userRaw.Nickname,
|
||||||
&userRaw.PasswordHash,
|
&userRaw.PasswordHash,
|
||||||
&userRaw.OpenID,
|
&userRaw.OpenID,
|
||||||
&userRaw.CreatedTs,
|
&userRaw.CreatedTs,
|
||||||
@ -236,11 +241,14 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
|||||||
if v := patch.RowStatus; v != nil {
|
if v := patch.RowStatus; v != nil {
|
||||||
set, args = append(set, "row_status = ?"), append(args, *v)
|
set, args = append(set, "row_status = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
|
if v := patch.Username; v != nil {
|
||||||
|
set, args = append(set, "username = ?"), append(args, *v)
|
||||||
|
}
|
||||||
if v := patch.Email; v != nil {
|
if v := patch.Email; v != nil {
|
||||||
set, args = append(set, "email = ?"), append(args, *v)
|
set, args = append(set, "email = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
if v := patch.Name; v != nil {
|
if v := patch.Nickname; v != nil {
|
||||||
set, args = append(set, "name = ?"), append(args, *v)
|
set, args = append(set, "nickname = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
if v := patch.PasswordHash; v != nil {
|
if v := patch.PasswordHash; v != nil {
|
||||||
set, args = append(set, "password_hash = ?"), append(args, *v)
|
set, args = append(set, "password_hash = ?"), append(args, *v)
|
||||||
@ -255,38 +263,25 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
|||||||
UPDATE user
|
UPDATE user
|
||||||
SET ` + strings.Join(set, ", ") + `
|
SET ` + strings.Join(set, ", ") + `
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
|
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
|
||||||
`
|
`
|
||||||
row, err := tx.QueryContext(ctx, query, args...)
|
var userRaw userRaw
|
||||||
if err != nil {
|
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||||
|
&userRaw.ID,
|
||||||
|
&userRaw.Username,
|
||||||
|
&userRaw.Role,
|
||||||
|
&userRaw.Email,
|
||||||
|
&userRaw.Nickname,
|
||||||
|
&userRaw.PasswordHash,
|
||||||
|
&userRaw.OpenID,
|
||||||
|
&userRaw.CreatedTs,
|
||||||
|
&userRaw.UpdatedTs,
|
||||||
|
&userRaw.RowStatus,
|
||||||
|
); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
}
|
}
|
||||||
defer row.Close()
|
|
||||||
|
|
||||||
if row.Next() {
|
return &userRaw, nil
|
||||||
var userRaw userRaw
|
|
||||||
if err := row.Scan(
|
|
||||||
&userRaw.ID,
|
|
||||||
&userRaw.Email,
|
|
||||||
&userRaw.Role,
|
|
||||||
&userRaw.Name,
|
|
||||||
&userRaw.PasswordHash,
|
|
||||||
&userRaw.OpenID,
|
|
||||||
&userRaw.CreatedTs,
|
|
||||||
&userRaw.UpdatedTs,
|
|
||||||
&userRaw.RowStatus,
|
|
||||||
); err != nil {
|
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := row.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &userRaw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) {
|
func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) {
|
||||||
@ -295,14 +290,17 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
|||||||
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.Username; v != nil {
|
||||||
|
where, args = append(where, "username = ?"), append(args, *v)
|
||||||
|
}
|
||||||
if v := find.Role; v != nil {
|
if v := find.Role; v != nil {
|
||||||
where, args = append(where, "role = ?"), append(args, *v)
|
where, args = append(where, "role = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
if v := find.Email; v != nil {
|
if v := find.Email; v != nil {
|
||||||
where, args = append(where, "email = ?"), append(args, *v)
|
where, args = append(where, "email = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
if v := find.Name; v != nil {
|
if v := find.Nickname; v != nil {
|
||||||
where, args = append(where, "name = ?"), append(args, *v)
|
where, args = append(where, "nickname = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
if v := find.OpenID; v != nil {
|
if v := find.OpenID; v != nil {
|
||||||
where, args = append(where, "open_id = ?"), append(args, *v)
|
where, args = append(where, "open_id = ?"), append(args, *v)
|
||||||
@ -311,9 +309,10 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
|||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
email,
|
username,
|
||||||
role,
|
role,
|
||||||
name,
|
email,
|
||||||
|
nickname,
|
||||||
password_hash,
|
password_hash,
|
||||||
open_id,
|
open_id,
|
||||||
created_ts,
|
created_ts,
|
||||||
@ -334,9 +333,10 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
|||||||
var userRaw userRaw
|
var userRaw userRaw
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&userRaw.ID,
|
&userRaw.ID,
|
||||||
&userRaw.Email,
|
&userRaw.Username,
|
||||||
&userRaw.Role,
|
&userRaw.Role,
|
||||||
&userRaw.Name,
|
&userRaw.Email,
|
||||||
|
&userRaw.Nickname,
|
||||||
&userRaw.PasswordHash,
|
&userRaw.PasswordHash,
|
||||||
&userRaw.OpenID,
|
&userRaw.OpenID,
|
||||||
&userRaw.CreatedTs,
|
&userRaw.CreatedTs,
|
||||||
|
@ -5,7 +5,6 @@ import { userService } from "../services";
|
|||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
import "../less/change-password-dialog.less";
|
|
||||||
|
|
||||||
const validateConfig: ValidatorConfig = {
|
const validateConfig: ValidatorConfig = {
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
@ -73,29 +72,34 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container !w-64">
|
||||||
<p className="title-text">{t("setting.account-section.change-password")}</p>
|
<p className="title-text">{t("setting.account-section.change-password")}</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<Icon.X />
|
<Icon.X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<label className="form-label input-form-label">
|
<p className="text-sm mb-1">{t("common.new-password")}</p>
|
||||||
<input type="password" placeholder={t("common.new-password")} value={newPassword} onChange={handleNewPasswordChanged} />
|
<input
|
||||||
</label>
|
type="password"
|
||||||
<label className="form-label input-form-label">
|
className="input-text"
|
||||||
<input
|
placeholder={t("common.repeat-new-password")}
|
||||||
type="password"
|
value={newPassword}
|
||||||
placeholder={t("common.repeat-new-password")}
|
onChange={handleNewPasswordChanged}
|
||||||
value={newPasswordAgain}
|
/>
|
||||||
onChange={handleNewPasswordAgainChanged}
|
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
|
||||||
/>
|
<input
|
||||||
</label>
|
type="password"
|
||||||
<div className="btns-container">
|
className="input-text"
|
||||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
placeholder={t("common.repeat-new-password")}
|
||||||
|
value={newPasswordAgain}
|
||||||
|
onChange={handleNewPasswordAgainChanged}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
|
||||||
|
<span className="btn-text" onClick={handleCloseBtnClick}>
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</span>
|
</span>
|
||||||
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
|
<span className="btn-primary" onClick={handleSaveBtnClick}>
|
||||||
{t("common.save")}
|
{t("common.save")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@ import { showCommonDialog } from "../Dialog/CommonDialog";
|
|||||||
import "../../less/settings/member-section.less";
|
import "../../less/settings/member-section.less";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
createUserEmail: string;
|
createUserUsername: string;
|
||||||
createUserPassword: string;
|
createUserPassword: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const PreferencesSection = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentUser = useAppSelector((state) => state.user.user);
|
const currentUser = useAppSelector((state) => state.user.user);
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
createUserEmail: "",
|
createUserUsername: "",
|
||||||
createUserPassword: "",
|
createUserPassword: "",
|
||||||
});
|
});
|
||||||
const [userList, setUserList] = useState<User[]>([]);
|
const [userList, setUserList] = useState<User[]>([]);
|
||||||
@ -31,10 +31,10 @@ const PreferencesSection = () => {
|
|||||||
setUserList(data);
|
setUserList(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmailInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUsernameInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
createUserEmail: event.target.value,
|
createUserUsername: event.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,16 +46,15 @@ const PreferencesSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateUserBtnClick = async () => {
|
const handleCreateUserBtnClick = async () => {
|
||||||
if (state.createUserEmail === "" || state.createUserPassword === "") {
|
if (state.createUserUsername === "" || state.createUserPassword === "") {
|
||||||
toastHelper.error(t("message.fill-form"));
|
toastHelper.error(t("message.fill-form"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userCreate: UserCreate = {
|
const userCreate: UserCreate = {
|
||||||
email: state.createUserEmail,
|
username: state.createUserUsername,
|
||||||
password: state.createUserPassword,
|
password: state.createUserPassword,
|
||||||
role: "USER",
|
role: "USER",
|
||||||
name: state.createUserEmail,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -66,7 +65,7 @@ const PreferencesSection = () => {
|
|||||||
}
|
}
|
||||||
await fetchUserList();
|
await fetchUserList();
|
||||||
setState({
|
setState({
|
||||||
createUserEmail: "",
|
createUserUsername: "",
|
||||||
createUserPassword: "",
|
createUserPassword: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -74,7 +73,7 @@ const PreferencesSection = () => {
|
|||||||
const handleArchiveUserClick = (user: User) => {
|
const handleArchiveUserClick = (user: User) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: `Archive Member`,
|
title: `Archive Member`,
|
||||||
content: `❗️Are you sure to archive ${user.name}?`,
|
content: `❗️Are you sure to archive ${user.username}?`,
|
||||||
style: "warning",
|
style: "warning",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await userService.patchUser({
|
await userService.patchUser({
|
||||||
@ -97,7 +96,7 @@ const PreferencesSection = () => {
|
|||||||
const handleDeleteUserClick = (user: User) => {
|
const handleDeleteUserClick = (user: User) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: `Delete Member`,
|
title: `Delete Member`,
|
||||||
content: `Are you sure to delete ${user.name}? THIS ACTION IS IRREVERSIABLE.❗️`,
|
content: `Are you sure to delete ${user.username}? THIS ACTION IS IRREVERSIABLE.❗️`,
|
||||||
style: "warning",
|
style: "warning",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await userService.deleteUser({
|
await userService.deleteUser({
|
||||||
@ -113,8 +112,8 @@ const PreferencesSection = () => {
|
|||||||
<p className="title-text">{t("setting.member-section.create-a-member")}</p>
|
<p className="title-text">{t("setting.member-section.create-a-member")}</p>
|
||||||
<div className="create-member-container">
|
<div className="create-member-container">
|
||||||
<div className="input-form-container">
|
<div className="input-form-container">
|
||||||
<span className="field-text">{t("common.email")}</span>
|
<span className="field-text">{t("common.username")}</span>
|
||||||
<input type="email" placeholder={t("common.email")} value={state.createUserEmail} onChange={handleEmailInputChange} />
|
<input type="text" placeholder={t("common.username")} value={state.createUserUsername} onChange={handleUsernameInputChange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="input-form-container">
|
<div className="input-form-container">
|
||||||
<span className="field-text">{t("common.password")}</span>
|
<span className="field-text">{t("common.password")}</span>
|
||||||
@ -127,13 +126,13 @@ const PreferencesSection = () => {
|
|||||||
<p className="title-text">{t("setting.member-list")}</p>
|
<p className="title-text">{t("setting.member-list")}</p>
|
||||||
<div className="member-container field-container">
|
<div className="member-container field-container">
|
||||||
<span className="field-text">ID</span>
|
<span className="field-text">ID</span>
|
||||||
<span className="field-text">{t("common.email")}</span>
|
<span className="field-text username-field">{t("common.username")}</span>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
{userList.map((user) => (
|
{userList.map((user) => (
|
||||||
<div key={user.id} className={`member-container ${user.rowStatus === "ARCHIVED" ? "archived" : ""}`}>
|
<div key={user.id} className={`member-container ${user.rowStatus === "ARCHIVED" ? "archived" : ""}`}>
|
||||||
<span className="field-text id-text">{user.id}</span>
|
<span className="field-text id-text">{user.id}</span>
|
||||||
<span className="field-text email-text">{user.email}</span>
|
<span className="field-text username-text">{user.username}</span>
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
{currentUser?.id === user.id ? (
|
{currentUser?.id === user.id ? (
|
||||||
<span className="tip-text">{t("common.yourself")}</span>
|
<span className="tip-text">{t("common.yourself")}</span>
|
||||||
|
@ -1,58 +1,16 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAppSelector } from "../../store";
|
import { useAppSelector } from "../../store";
|
||||||
import { userService } from "../../services";
|
import { userService } from "../../services";
|
||||||
import { validate, ValidatorConfig } from "../../helpers/validator";
|
|
||||||
import toastHelper from "../Toast";
|
|
||||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||||
import showChangePasswordDialog from "../ChangePasswordDialog";
|
import showChangePasswordDialog from "../ChangePasswordDialog";
|
||||||
|
import showUpdateAccountDialog from "../UpdateAccountDialog";
|
||||||
import "../../less/settings/my-account-section.less";
|
import "../../less/settings/my-account-section.less";
|
||||||
|
|
||||||
const validateConfig: ValidatorConfig = {
|
|
||||||
minLength: 1,
|
|
||||||
maxLength: 24,
|
|
||||||
noSpace: true,
|
|
||||||
noChinese: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MyAccountSection = () => {
|
const MyAccountSection = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const user = useAppSelector((state) => state.user.user as User);
|
const user = useAppSelector((state) => state.user.user as User);
|
||||||
const [username, setUsername] = useState<string>(user.name);
|
|
||||||
const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`;
|
const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`;
|
||||||
|
|
||||||
const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const nextUsername = e.target.value as string;
|
|
||||||
setUsername(nextUsername);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmEditUsernameBtnClick = async () => {
|
|
||||||
if (username === user.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const usernameValidResult = validate(username, validateConfig);
|
|
||||||
if (!usernameValidResult.result) {
|
|
||||||
toastHelper.error(t("common.username") + i18n.language === "zh" ? "" : " " + usernameValidResult.reason);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await userService.patchUser({
|
|
||||||
id: user.id,
|
|
||||||
name: username,
|
|
||||||
});
|
|
||||||
toastHelper.info(t("common.username") + i18n.language === "zh" ? "" : " " + t("common.changed"));
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
toastHelper.error(error.response.data.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangePasswordBtnClick = () => {
|
|
||||||
showChangePasswordDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetOpenIdBtnClick = async () => {
|
const handleResetOpenIdBtnClick = async () => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: "Reset Open API",
|
title: "Reset Open API",
|
||||||
@ -67,42 +25,23 @@ const MyAccountSection = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreventDefault = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="section-container account-section-container">
|
<div className="section-container account-section-container">
|
||||||
<p className="title-text">{t("setting.account-section.title")}</p>
|
<p className="title-text">{t("setting.account-section.title")}</p>
|
||||||
<label className="form-label">
|
<div className="flex flex-row justify-start items-end">
|
||||||
<span className="normal-text">{t("common.email")}:</span>
|
<span className="text-2xl leading-10 font-medium">{user.nickname}</span>
|
||||||
<span className="normal-text">{user.email}</span>
|
<span className="text-base ml-1 text-gray-500 leading-8">({user.username})</span>
|
||||||
</label>
|
</div>
|
||||||
<label className="form-label input-form-label username-label">
|
<div className="flex flex-row justify-start items-center text-base text-gray-600">{user.email}</div>
|
||||||
<span className="normal-text">{t("common.username")}:</span>
|
<div className="w-full flex flex-row justify-start items-center mt-2 space-x-2">
|
||||||
<input type="text" value={username} onChange={handleUsernameChanged} />
|
<button className="px-2 py-1 border rounded-md text-sm hover:bg-gray-100" onClick={showUpdateAccountDialog}>
|
||||||
<div className={`btns-container ${username === user.name ? "!hidden" : ""}`} onClick={handlePreventDefault}>
|
Update Information
|
||||||
<span className="btn confirm-btn" onClick={handleConfirmEditUsernameBtnClick}>
|
</button>
|
||||||
{t("common.save")}
|
<button className="px-2 py-1 border rounded-md text-sm hover:bg-gray-100" onClick={showChangePasswordDialog}>
|
||||||
</span>
|
Change Password
|
||||||
<span
|
</button>
|
||||||
className="btn cancel-btn"
|
</div>
|
||||||
onClick={() => {
|
|
||||||
setUsername(user.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common.cancel")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label className="form-label password-label">
|
|
||||||
<span className="normal-text">{t("common.password")}:</span>
|
|
||||||
<span className="btn" onClick={handleChangePasswordBtnClick}>
|
|
||||||
{t("common.change")}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="section-container openapi-section-container">
|
<div className="section-container openapi-section-container">
|
||||||
<p className="title-text">Open API</p>
|
<p className="title-text">Open API</p>
|
||||||
|
@ -118,7 +118,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="watermark-container">
|
<div className="watermark-container">
|
||||||
<div className="userinfo-container">
|
<div className="userinfo-container">
|
||||||
<span className="name-text">{user.name}</span>
|
<span className="name-text">{user.nickname || user.username}</span>
|
||||||
<span className="usage-text">
|
<span className="usage-text">
|
||||||
{createdDays} DAYS / {state.memoAmount} MEMOS
|
{createdDays} DAYS / {state.memoAmount} MEMOS
|
||||||
</span>
|
</span>
|
||||||
|
118
web/src/components/UpdateAccountDialog.tsx
Normal file
118
web/src/components/UpdateAccountDialog.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAppSelector } from "../store";
|
||||||
|
import { userService } from "../services";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import { generateDialog } from "./Dialog";
|
||||||
|
import toastHelper from "./Toast";
|
||||||
|
|
||||||
|
type Props = DialogProps;
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const user = useAppSelector((state) => state.user.user as User);
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
username: user.username,
|
||||||
|
nickname: user.nickname,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// do nth
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCloseBtnClick = () => {
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNicknameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nickname: e.target.value as string,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
username: e.target.value as string,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleEmailChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
email: e.target.value as string,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveBtnClick = async () => {
|
||||||
|
if (state.username === "") {
|
||||||
|
toastHelper.error(t("message.fill-all"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = userService.getState().user as User;
|
||||||
|
await userService.patchUser({
|
||||||
|
id: user.id,
|
||||||
|
username: state.username,
|
||||||
|
nickname: state.nickname,
|
||||||
|
email: state.email,
|
||||||
|
});
|
||||||
|
toastHelper.info("Update succeed");
|
||||||
|
handleCloseBtnClick();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
toastHelper.error(error.response.data.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dialog-header-container !w-64">
|
||||||
|
<p className="title-text">Update information</p>
|
||||||
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
|
<Icon.X />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-content-container">
|
||||||
|
<p className="text-sm mb-1">Nickname</p>
|
||||||
|
<input type="text" className="input-text" value={state.nickname} onChange={handleNicknameChanged} />
|
||||||
|
<p className="text-sm mb-1 mt-2">Username</p>
|
||||||
|
<input type="text" className="input-text" value={state.username} onChange={handleUsernameChanged} />
|
||||||
|
<p className="text-sm mb-1 mt-2">Email</p>
|
||||||
|
<input type="text" className="input-text" value={state.email} onChange={handleEmailChanged} />
|
||||||
|
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
|
||||||
|
<span className="btn-text" onClick={handleCloseBtnClick}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</span>
|
||||||
|
<span className="btn-primary" onClick={handleSaveBtnClick}>
|
||||||
|
{t("common.save")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function showUpdateAccountDialog() {
|
||||||
|
generateDialog(
|
||||||
|
{
|
||||||
|
className: "update-account-dialog",
|
||||||
|
},
|
||||||
|
UpdateAccountDialog
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default showUpdateAccountDialog;
|
@ -123,7 +123,7 @@ const UsageHeatMap = () => {
|
|||||||
})}
|
})}
|
||||||
{nullCell.map((_, i) => (
|
{nullCell.map((_, i) => (
|
||||||
<div className="stat-wrapper" key={i}>
|
<div className="stat-wrapper" key={i}>
|
||||||
<span className="stat-container null"></span>
|
<span className="null"></span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,10 +28,10 @@ const UserBanner = () => {
|
|||||||
if (!owner) {
|
if (!owner) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUsername(owner.name);
|
setUsername(owner.nickname || owner.username);
|
||||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(owner.createdTs)) / 1000 / 3600 / 24));
|
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(owner.createdTs)) / 1000 / 3600 / 24));
|
||||||
} else if (user) {
|
} else if (user) {
|
||||||
setUsername(user.name);
|
setUsername(user.nickname || user.username);
|
||||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
|
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
|
||||||
}
|
}
|
||||||
}, [isVisitorMode, user, owner]);
|
}, [isVisitorMode, user, owner]);
|
||||||
|
@ -13,3 +13,15 @@
|
|||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply select-none inline-flex border border-transparent cursor-pointer px-3 bg-green-600 text-sm leading-8 text-white rounded-md hover:opacity-80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
@apply select-none inline-flex border border-transparent cursor-pointer px-2 text-sm text-gray-600 leading-8 hover:opacity-80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-text {
|
||||||
|
@apply w-full px-3 py-2 leading-6 text-sm border rounded;
|
||||||
|
}
|
||||||
|
@ -14,19 +14,18 @@ export function upsertSystemSetting(systemSetting: SystemSetting) {
|
|||||||
return axios.post<ResponseObject<SystemSetting>>("/api/system/setting", systemSetting);
|
return axios.post<ResponseObject<SystemSetting>>("/api/system/setting", systemSetting);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signin(email: string, password: string) {
|
export function signin(username: string, password: string) {
|
||||||
return axios.post<ResponseObject<User>>("/api/auth/signin", {
|
return axios.post<ResponseObject<User>>("/api/auth/signin", {
|
||||||
email,
|
username,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signup(email: string, password: string, role: UserRole) {
|
export function signup(username: string, password: string, role: UserRole) {
|
||||||
return axios.post<ResponseObject<User>>("/api/auth/signup", {
|
return axios.post<ResponseObject<User>>("/api/auth/signup", {
|
||||||
email,
|
username,
|
||||||
password,
|
password,
|
||||||
role,
|
role,
|
||||||
name: email,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
.change-password-dialog {
|
|
||||||
> .dialog-container {
|
|
||||||
@apply w-72;
|
|
||||||
|
|
||||||
> .dialog-content-container {
|
|
||||||
@apply flex flex-col justify-start items-start;
|
|
||||||
|
|
||||||
> .tip-text {
|
|
||||||
@apply bg-gray-400 text-xs p-2 rounded-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .form-label {
|
|
||||||
@apply flex flex-col justify-start items-start;
|
|
||||||
@apply relative w-full leading-relaxed;
|
|
||||||
|
|
||||||
&.input-form-label {
|
|
||||||
@apply py-3 pb-1;
|
|
||||||
|
|
||||||
> input {
|
|
||||||
@apply w-full p-2 text-sm leading-6 rounded border border-gray-400 bg-transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .btns-container {
|
|
||||||
@apply mt-2 w-full flex flex-row justify-end items-center;
|
|
||||||
|
|
||||||
> .btn {
|
|
||||||
@apply text-sm px-4 py-2 rounded ml-2 bg-gray-400;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply opacity-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.confirm-btn {
|
|
||||||
@apply bg-green-600 text-white shadow-inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cancel-btn {
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,8 +2,7 @@
|
|||||||
@apply flex flex-col justify-start items-start relative w-full h-auto bg-white;
|
@apply flex flex-col justify-start items-start relative w-full h-auto bg-white;
|
||||||
|
|
||||||
> .common-editor-inputer {
|
> .common-editor-inputer {
|
||||||
@apply w-full h-full my-1 text-base resize-none overflow-x-hidden overflow-y-auto bg-transparent whitespace-pre-wrap;
|
@apply w-full h-full ~"max-h-[300px]" my-1 text-base resize-none overflow-x-hidden overflow-y-auto bg-transparent outline-none whitespace-pre-wrap;
|
||||||
max-height: 300px;
|
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
|
@ -5,46 +5,3 @@ html {
|
|||||||
"WenQuanYi Micro Hei", sans-serif, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
"WenQuanYi Micro Hei", sans-serif, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||||
"Noto Color Emoji";
|
"Noto Color Emoji";
|
||||||
}
|
}
|
||||||
|
|
||||||
label,
|
|
||||||
button,
|
|
||||||
img {
|
|
||||||
@apply bg-transparent select-none outline-none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
@apply appearance-none outline-none !important;
|
|
||||||
@apply bg-transparent;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:-webkit-autofill,
|
|
||||||
input:-webkit-autofill:hover,
|
|
||||||
input:-webkit-autofill:focus,
|
|
||||||
input:-webkit-autofill:active {
|
|
||||||
@apply shadow-inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style-type: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
@apply font-bold mr-1;
|
|
||||||
content: "•";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@apply cursor-pointer text-blue-600 underline underline-offset-2 hover:opacity-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
code,
|
|
||||||
pre {
|
|
||||||
@apply break-all whitespace-pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
@apply select-none cursor-pointer text-center;
|
|
||||||
}
|
|
||||||
|
@ -45,6 +45,15 @@
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply font-bold mr-1;
|
||||||
|
content: "•";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
@apply w-full my-1 p-3 rounded bg-gray-100 whitespace-pre-wrap;
|
@apply w-full my-1 p-3 rounded bg-gray-100 whitespace-pre-wrap;
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .text-input {
|
> .text-input {
|
||||||
@apply hidden sm:flex ml-2 w-24 grow text-sm;
|
@apply hidden sm:flex ml-2 w-24 grow text-sm outline-none bg-transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,10 @@
|
|||||||
> .field-container {
|
> .field-container {
|
||||||
> .field-text {
|
> .field-text {
|
||||||
@apply text-gray-400 text-sm;
|
@apply text-gray-400 text-sm;
|
||||||
|
|
||||||
|
&.username-field {
|
||||||
|
@apply col-span-2 w-full;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +43,7 @@
|
|||||||
@apply font-mono text-gray-600;
|
@apply font-mono text-gray-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.email-text {
|
&.username-text {
|
||||||
@apply w-auto col-span-2;
|
@apply w-auto col-span-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,3 @@
|
|||||||
.account-section-container {
|
|
||||||
> .form-label {
|
|
||||||
min-height: 28px;
|
|
||||||
|
|
||||||
> .normal-text {
|
|
||||||
@apply first:mr-2 text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.username-label {
|
|
||||||
@apply w-full flex-wrap;
|
|
||||||
|
|
||||||
> input {
|
|
||||||
@apply grow-0 w-32 shadow-inner px-2 mr-2 text-sm border rounded leading-7 bg-transparent focus:border-black;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .btns-container {
|
|
||||||
@apply shrink-0 grow flex flex-row justify-start items-center;
|
|
||||||
|
|
||||||
> .btn {
|
|
||||||
@apply text-sm shadow px-2 leading-7 rounded border hover:opacity-80 bg-gray-50;
|
|
||||||
|
|
||||||
&.cancel-btn {
|
|
||||||
@apply shadow-none border-none bg-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.confirm-btn {
|
|
||||||
@apply bg-green-600 border-green-600 text-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.password-label {
|
|
||||||
> .btn {
|
|
||||||
@apply text-blue-600 text-sm ml-1 cursor-pointer hover:opacity-80;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-section-container {
|
.openapi-section-container {
|
||||||
> .value-text {
|
> .value-text {
|
||||||
@apply w-full font-mono text-sm shadow-inner border py-2 px-3 rounded leading-6 break-all whitespace-pre-wrap;
|
@apply w-full font-mono text-sm shadow-inner border py-2 px-3 rounded leading-6 break-all whitespace-pre-wrap;
|
||||||
|
@ -22,10 +22,6 @@
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
|
|
||||||
&.null {
|
|
||||||
@apply bg-gray-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stat-day-l1-bg {
|
&.stat-day-l1-bg {
|
||||||
@apply bg-green-400;
|
@apply bg-green-400;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"new-password": "New passworld",
|
"new-password": "New passworld",
|
||||||
"repeat-new-password": "Repeat the new password",
|
"repeat-new-password": "Repeat the new password",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
"nickname": "Nickname",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"new-password": "Mật khẩu mới",
|
"new-password": "Mật khẩu mới",
|
||||||
"repeat-new-password": "Nhập lại mật khẩu mới",
|
"repeat-new-password": "Nhập lại mật khẩu mới",
|
||||||
"username": "Tên đăng nhập",
|
"username": "Tên đăng nhập",
|
||||||
|
"nickname": "Nickname",
|
||||||
"save": "Lưu",
|
"save": "Lưu",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"cancel": "Hủy",
|
"cancel": "Hủy",
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"new-password": "新密码",
|
"new-password": "新密码",
|
||||||
"repeat-new-password": "重复新密码",
|
"repeat-new-password": "重复新密码",
|
||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
|
"nickname": "昵称",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"cancel": "退出",
|
"cancel": "退出",
|
||||||
|
@ -23,12 +23,12 @@ const Auth = () => {
|
|||||||
const systemStatus = useAppSelector((state) => state.global.systemStatus);
|
const systemStatus = useAppSelector((state) => state.global.systemStatus);
|
||||||
const actionBtnLoadingState = useLoading(false);
|
const actionBtnLoadingState = useLoading(false);
|
||||||
const mode = systemStatus.profile.mode;
|
const mode = systemStatus.profile.mode;
|
||||||
const [email, setEmail] = useState(mode === "dev" ? "demo@usememos.com" : "");
|
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
||||||
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
||||||
|
|
||||||
const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const text = e.target.value as string;
|
const text = e.target.value as string;
|
||||||
setEmail(text);
|
setUsername(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -41,9 +41,9 @@ const Auth = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailValidResult = validate(email, validateConfig);
|
const usernameValidResult = validate(username, validateConfig);
|
||||||
if (!emailValidResult.result) {
|
if (!usernameValidResult.result) {
|
||||||
toastHelper.error(t("common.email") + ": " + emailValidResult.reason);
|
toastHelper.error(t("common.username") + ": " + usernameValidResult.reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ const Auth = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
actionBtnLoadingState.setLoading();
|
actionBtnLoadingState.setLoading();
|
||||||
await api.signin(email, password);
|
await api.signin(username, password);
|
||||||
const user = await userService.doSignIn();
|
const user = await userService.doSignIn();
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
@ -74,9 +74,9 @@ const Auth = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailValidResult = validate(email, validateConfig);
|
const usernameValidResult = validate(username, validateConfig);
|
||||||
if (!emailValidResult.result) {
|
if (!usernameValidResult.result) {
|
||||||
toastHelper.error(t("common.email") + ": " + emailValidResult.reason);
|
toastHelper.error(t("common.username") + ": " + usernameValidResult.reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ const Auth = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
actionBtnLoadingState.setLoading();
|
actionBtnLoadingState.setLoading();
|
||||||
await api.signup(email, password, role);
|
await api.signup(username, password, role);
|
||||||
const user = await userService.doSignIn();
|
const user = await userService.doSignIn();
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
@ -118,8 +118,8 @@ const Auth = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={`page-content-container ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}>
|
<div className={`page-content-container ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}>
|
||||||
<div className="form-item-container input-form-container">
|
<div className="form-item-container input-form-container">
|
||||||
<span className={`normal-text ${email ? "not-null" : ""}`}>{t("common.email")}</span>
|
<span className={`normal-text ${username ? "not-null" : ""}`}>{t("common.username")}</span>
|
||||||
<input type="email" value={email} onChange={handleEmailInputChanged} required />
|
<input type="text" value={username} onChange={handleUsernameInputChanged} required />
|
||||||
</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" : ""}`}>{t("common.password")}</span>
|
<span className={`normal-text ${password ? "not-null" : ""}`}>{t("common.password")}</span>
|
||||||
|
@ -83,7 +83,7 @@ const Explore = () => {
|
|||||||
<div className="memo-header">
|
<div className="memo-header">
|
||||||
<span className="time-text">{createdAtStr}</span>
|
<span className="time-text">{createdAtStr}</span>
|
||||||
<a className="name-text" href={`/u/${memo.creator.id}`}>
|
<a className="name-text" href={`/u/${memo.creator.id}`}>
|
||||||
@{memo.creator.name}
|
@{memo.creator.nickname || memo.creator.username}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<MemoContent className="memo-content" content={memo.content} onMemoContentClick={() => undefined} />
|
<MemoContent className="memo-content" content={memo.content} onMemoContentClick={() => undefined} />
|
||||||
|
@ -83,7 +83,7 @@ const MemoDetail = () => {
|
|||||||
<div className="status-container">
|
<div className="status-container">
|
||||||
<span className="time-text">{dayjs(state.memo.displayTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss")}</span>
|
<span className="time-text">{dayjs(state.memo.displayTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss")}</span>
|
||||||
<a className="name-text" href={`/u/${state.memo.creator.id}`}>
|
<a className="name-text" href={`/u/${state.memo.creator.id}`}>
|
||||||
@{state.memo.creator.name}
|
@{state.memo.creator.nickname || state.memo.creator.username}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
12
web/src/types/modules/user.d.ts
vendored
12
web/src/types/modules/user.d.ts
vendored
@ -8,9 +8,10 @@ interface User {
|
|||||||
updatedTs: TimeStamp;
|
updatedTs: TimeStamp;
|
||||||
rowStatus: RowStatus;
|
rowStatus: RowStatus;
|
||||||
|
|
||||||
|
username: string;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
nickname: string;
|
||||||
openId: string;
|
openId: string;
|
||||||
userSettingList: UserSetting[];
|
userSettingList: UserSetting[];
|
||||||
|
|
||||||
@ -18,18 +19,17 @@ interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UserCreate {
|
interface UserCreate {
|
||||||
email: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
name: string;
|
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserPatch {
|
interface UserPatch {
|
||||||
id: UserId;
|
id: UserId;
|
||||||
|
|
||||||
rowStatus?: RowStatus;
|
rowStatus?: RowStatus;
|
||||||
|
username?: string;
|
||||||
name?: string;
|
email?: string;
|
||||||
|
nickname?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
resetOpenId?: boolean;
|
resetOpenId?: boolean;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user