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
|
||||
|
||||
type Signin struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type Signup struct {
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
35
api/user.go
35
api/user.go
@ -2,8 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
)
|
||||
|
||||
// Role is the type of a role.
|
||||
@ -12,6 +10,8 @@ type Role string
|
||||
const (
|
||||
// Host is the HOST role.
|
||||
Host Role = "HOST"
|
||||
// Admin is the ADMIN role.
|
||||
Admin Role = "ADMIN"
|
||||
// NormalUser is the USER role.
|
||||
NormalUser Role = "USER"
|
||||
)
|
||||
@ -20,6 +20,8 @@ func (e Role) String() string {
|
||||
switch e {
|
||||
case Host:
|
||||
return "HOST"
|
||||
case Admin:
|
||||
return "ADMIN"
|
||||
case NormalUser:
|
||||
return "USER"
|
||||
}
|
||||
@ -35,9 +37,10 @@ type User struct {
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
|
||||
// Domain specific fields
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
PasswordHash string `json:"-"`
|
||||
OpenID string `json:"openId"`
|
||||
UserSettingList []*UserSetting `json:"userSettingList"`
|
||||
@ -45,23 +48,21 @@ type User struct {
|
||||
|
||||
type UserCreate struct {
|
||||
// Domain specific fields
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
PasswordHash string
|
||||
OpenID string
|
||||
}
|
||||
|
||||
func (create UserCreate) Validate() error {
|
||||
if !common.ValidateEmail(create.Email) {
|
||||
return fmt.Errorf("invalid email format")
|
||||
if len(create.Username) < 4 {
|
||||
return fmt.Errorf("username is too short, minimum length is 4")
|
||||
}
|
||||
if len(create.Email) < 6 {
|
||||
return fmt.Errorf("email is too short, minimum length is 6")
|
||||
}
|
||||
if len(create.Password) < 6 {
|
||||
return fmt.Errorf("password is too short, minimum length is 6")
|
||||
if len(create.Password) < 4 {
|
||||
return fmt.Errorf("password is too short, minimum length is 4")
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -75,8 +76,9 @@ type UserPatch struct {
|
||||
RowStatus *RowStatus `json:"rowStatus"`
|
||||
|
||||
// Domain specific fields
|
||||
Username *string `json:"username"`
|
||||
Email *string `json:"email"`
|
||||
Name *string `json:"name"`
|
||||
Nickname *string `json:"nickname"`
|
||||
Password *string `json:"password"`
|
||||
ResetOpenID *bool `json:"resetOpenId"`
|
||||
PasswordHash *string
|
||||
@ -90,9 +92,10 @@ type UserFind struct {
|
||||
RowStatus *RowStatus `json:"rowStatus"`
|
||||
|
||||
// Domain specific fields
|
||||
Email *string `json:"email"`
|
||||
Username *string `json:"username"`
|
||||
Role *Role
|
||||
Name *string `json:"name"`
|
||||
Email *string `json:"email"`
|
||||
Nickname *string `json:"nickname"`
|
||||
OpenID *string
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,7 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
if user != nil {
|
||||
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)
|
||||
}
|
||||
|
@ -22,16 +22,16 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
userFind := &api.UserFind{
|
||||
Email: &signin.Email,
|
||||
Username: &signin.Username,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
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 {
|
||||
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 {
|
||||
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.
|
||||
@ -107,9 +107,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
userCreate := &api.UserCreate{
|
||||
Email: signup.Email,
|
||||
Username: signup.Username,
|
||||
Role: api.Role(signup.Role),
|
||||
Name: signup.Name,
|
||||
Nickname: signup.Username,
|
||||
Password: signup.Password,
|
||||
OpenID: common.GenUUID(),
|
||||
}
|
||||
|
@ -46,14 +46,14 @@ func (s *Server) registerRSSRoutes(g *echo.Group) {
|
||||
Title: "Memos",
|
||||
Link: &feeds.Link{Href: baseURL},
|
||||
Description: "Memos",
|
||||
Author: &feeds.Author{Name: user.Name},
|
||||
Author: &feeds.Author{Name: user.Username},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
feed.Items = make([]*feeds.Item, len(memoList))
|
||||
for i, memo := range memoList {
|
||||
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)},
|
||||
Description: memo.Content,
|
||||
Created: time.Unix(memo.CreatedTs, 0),
|
||||
|
@ -18,9 +18,10 @@ CREATE TABLE user (
|
||||
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',
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
|
||||
name TEXT NOT NULL,
|
||||
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
|
||||
);
|
||||
|
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
|
||||
user (
|
||||
`id`,
|
||||
`email`,
|
||||
`username`,
|
||||
`role`,
|
||||
`name`,
|
||||
`email`,
|
||||
`nickname`,
|
||||
`open_id`,
|
||||
`password_hash`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
101,
|
||||
'demo@usememos.com',
|
||||
'demohero',
|
||||
'HOST',
|
||||
'Demo Host',
|
||||
'demo@usememos.com',
|
||||
'Demo Hero',
|
||||
'demo_open_id',
|
||||
-- raw password: secret
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||
@ -21,17 +23,19 @@ VALUES
|
||||
INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`email`,
|
||||
`username`,
|
||||
`role`,
|
||||
`name`,
|
||||
`email`,
|
||||
`nickname`,
|
||||
`open_id`,
|
||||
`password_hash`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
102,
|
||||
'jack@usememos.com',
|
||||
'jack',
|
||||
'USER',
|
||||
'jack@usememos.com',
|
||||
'Jack',
|
||||
'jack_open_id',
|
||||
-- raw password: secret
|
||||
@ -42,9 +46,10 @@ INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`row_status`,
|
||||
`email`,
|
||||
`username`,
|
||||
`role`,
|
||||
`name`,
|
||||
`email`,
|
||||
`nickname`,
|
||||
`open_id`,
|
||||
`password_hash`
|
||||
)
|
||||
@ -52,8 +57,9 @@ VALUES
|
||||
(
|
||||
103,
|
||||
'ARCHIVED',
|
||||
'bob@usememos.com',
|
||||
'bob',
|
||||
'USER',
|
||||
'bob@usememos.com',
|
||||
'Bob',
|
||||
'bob_open_id',
|
||||
-- raw password: secret
|
||||
|
@ -21,9 +21,10 @@ type userRaw struct {
|
||||
UpdatedTs int64
|
||||
|
||||
// Domain specific fields
|
||||
Email string
|
||||
Username string
|
||||
Role api.Role
|
||||
Name string
|
||||
Email string
|
||||
Nickname string
|
||||
PasswordHash string
|
||||
OpenID string
|
||||
}
|
||||
@ -36,9 +37,10 @@ func (raw *userRaw) toUser() *api.User {
|
||||
CreatedTs: raw.CreatedTs,
|
||||
UpdatedTs: raw.UpdatedTs,
|
||||
|
||||
Email: raw.Email,
|
||||
Username: raw.Username,
|
||||
Role: raw.Role,
|
||||
Name: raw.Name,
|
||||
Email: raw.Email,
|
||||
Nickname: raw.Nickname,
|
||||
PasswordHash: raw.PasswordHash,
|
||||
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) {
|
||||
query := `
|
||||
INSERT INTO user (
|
||||
email,
|
||||
username,
|
||||
role,
|
||||
name,
|
||||
email,
|
||||
nickname,
|
||||
password_hash,
|
||||
open_id
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
|
||||
`
|
||||
var userRaw userRaw
|
||||
if err := tx.QueryRowContext(ctx, query,
|
||||
create.Email,
|
||||
create.Username,
|
||||
create.Role,
|
||||
create.Name,
|
||||
create.Email,
|
||||
create.Nickname,
|
||||
create.PasswordHash,
|
||||
create.OpenID,
|
||||
).Scan(
|
||||
&userRaw.ID,
|
||||
&userRaw.Email,
|
||||
&userRaw.Username,
|
||||
&userRaw.Role,
|
||||
&userRaw.Name,
|
||||
&userRaw.Email,
|
||||
&userRaw.Nickname,
|
||||
&userRaw.PasswordHash,
|
||||
&userRaw.OpenID,
|
||||
&userRaw.CreatedTs,
|
||||
@ -236,11 +241,14 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
||||
if v := patch.RowStatus; v != nil {
|
||||
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 {
|
||||
set, args = append(set, "email = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Name; v != nil {
|
||||
set, args = append(set, "name = ?"), append(args, *v)
|
||||
if v := patch.Nickname; v != nil {
|
||||
set, args = append(set, "nickname = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.PasswordHash; v != nil {
|
||||
set, args = append(set, "password_hash = ?"), append(args, *v)
|
||||
@ -255,21 +263,15 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
||||
UPDATE user
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
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...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if row.Next() {
|
||||
var userRaw userRaw
|
||||
if err := row.Scan(
|
||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
&userRaw.ID,
|
||||
&userRaw.Email,
|
||||
&userRaw.Username,
|
||||
&userRaw.Role,
|
||||
&userRaw.Name,
|
||||
&userRaw.Email,
|
||||
&userRaw.Nickname,
|
||||
&userRaw.PasswordHash,
|
||||
&userRaw.OpenID,
|
||||
&userRaw.CreatedTs,
|
||||
@ -279,14 +281,7 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
||||
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) {
|
||||
@ -295,14 +290,17 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
||||
if v := find.ID; v != nil {
|
||||
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 {
|
||||
where, args = append(where, "role = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Email; v != nil {
|
||||
where, args = append(where, "email = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Name; v != nil {
|
||||
where, args = append(where, "name = ?"), append(args, *v)
|
||||
if v := find.Nickname; v != nil {
|
||||
where, args = append(where, "nickname = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.OpenID; v != nil {
|
||||
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 := `
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
username,
|
||||
role,
|
||||
name,
|
||||
email,
|
||||
nickname,
|
||||
password_hash,
|
||||
open_id,
|
||||
created_ts,
|
||||
@ -334,9 +333,10 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
||||
var userRaw userRaw
|
||||
if err := rows.Scan(
|
||||
&userRaw.ID,
|
||||
&userRaw.Email,
|
||||
&userRaw.Username,
|
||||
&userRaw.Role,
|
||||
&userRaw.Name,
|
||||
&userRaw.Email,
|
||||
&userRaw.Nickname,
|
||||
&userRaw.PasswordHash,
|
||||
&userRaw.OpenID,
|
||||
&userRaw.CreatedTs,
|
||||
|
@ -5,7 +5,6 @@ import { userService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/change-password-dialog.less";
|
||||
|
||||
const validateConfig: ValidatorConfig = {
|
||||
minLength: 4,
|
||||
@ -73,29 +72,34 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<div className="dialog-header-container !w-64">
|
||||
<p className="title-text">{t("setting.account-section.change-password")}</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<label className="form-label input-form-label">
|
||||
<input type="password" placeholder={t("common.new-password")} value={newPassword} onChange={handleNewPasswordChanged} />
|
||||
</label>
|
||||
<label className="form-label input-form-label">
|
||||
<p className="text-sm mb-1">{t("common.new-password")}</p>
|
||||
<input
|
||||
type="password"
|
||||
className="input-text"
|
||||
placeholder={t("common.repeat-new-password")}
|
||||
value={newPassword}
|
||||
onChange={handleNewPasswordChanged}
|
||||
/>
|
||||
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
|
||||
<input
|
||||
type="password"
|
||||
className="input-text"
|
||||
placeholder={t("common.repeat-new-password")}
|
||||
value={newPasswordAgain}
|
||||
onChange={handleNewPasswordAgainChanged}
|
||||
/>
|
||||
</label>
|
||||
<div className="btns-container">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
<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 confirm-btn" onClick={handleSaveBtnClick}>
|
||||
<span className="btn-primary" onClick={handleSaveBtnClick}>
|
||||
{t("common.save")}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||
import "../../less/settings/member-section.less";
|
||||
|
||||
interface State {
|
||||
createUserEmail: string;
|
||||
createUserUsername: string;
|
||||
createUserPassword: string;
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ const PreferencesSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const currentUser = useAppSelector((state) => state.user.user);
|
||||
const [state, setState] = useState<State>({
|
||||
createUserEmail: "",
|
||||
createUserUsername: "",
|
||||
createUserPassword: "",
|
||||
});
|
||||
const [userList, setUserList] = useState<User[]>([]);
|
||||
@ -31,10 +31,10 @@ const PreferencesSection = () => {
|
||||
setUserList(data);
|
||||
};
|
||||
|
||||
const handleEmailInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleUsernameInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState({
|
||||
...state,
|
||||
createUserEmail: event.target.value,
|
||||
createUserUsername: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
@ -46,16 +46,15 @@ const PreferencesSection = () => {
|
||||
};
|
||||
|
||||
const handleCreateUserBtnClick = async () => {
|
||||
if (state.createUserEmail === "" || state.createUserPassword === "") {
|
||||
if (state.createUserUsername === "" || state.createUserPassword === "") {
|
||||
toastHelper.error(t("message.fill-form"));
|
||||
return;
|
||||
}
|
||||
|
||||
const userCreate: UserCreate = {
|
||||
email: state.createUserEmail,
|
||||
username: state.createUserUsername,
|
||||
password: state.createUserPassword,
|
||||
role: "USER",
|
||||
name: state.createUserEmail,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -66,7 +65,7 @@ const PreferencesSection = () => {
|
||||
}
|
||||
await fetchUserList();
|
||||
setState({
|
||||
createUserEmail: "",
|
||||
createUserUsername: "",
|
||||
createUserPassword: "",
|
||||
});
|
||||
};
|
||||
@ -74,7 +73,7 @@ const PreferencesSection = () => {
|
||||
const handleArchiveUserClick = (user: User) => {
|
||||
showCommonDialog({
|
||||
title: `Archive Member`,
|
||||
content: `❗️Are you sure to archive ${user.name}?`,
|
||||
content: `❗️Are you sure to archive ${user.username}?`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await userService.patchUser({
|
||||
@ -97,7 +96,7 @@ const PreferencesSection = () => {
|
||||
const handleDeleteUserClick = (user: User) => {
|
||||
showCommonDialog({
|
||||
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",
|
||||
onConfirm: async () => {
|
||||
await userService.deleteUser({
|
||||
@ -113,8 +112,8 @@ const PreferencesSection = () => {
|
||||
<p className="title-text">{t("setting.member-section.create-a-member")}</p>
|
||||
<div className="create-member-container">
|
||||
<div className="input-form-container">
|
||||
<span className="field-text">{t("common.email")}</span>
|
||||
<input type="email" placeholder={t("common.email")} value={state.createUserEmail} onChange={handleEmailInputChange} />
|
||||
<span className="field-text">{t("common.username")}</span>
|
||||
<input type="text" placeholder={t("common.username")} value={state.createUserUsername} onChange={handleUsernameInputChange} />
|
||||
</div>
|
||||
<div className="input-form-container">
|
||||
<span className="field-text">{t("common.password")}</span>
|
||||
@ -127,13 +126,13 @@ const PreferencesSection = () => {
|
||||
<p className="title-text">{t("setting.member-list")}</p>
|
||||
<div className="member-container field-container">
|
||||
<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>
|
||||
</div>
|
||||
{userList.map((user) => (
|
||||
<div key={user.id} className={`member-container ${user.rowStatus === "ARCHIVED" ? "archived" : ""}`}>
|
||||
<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">
|
||||
{currentUser?.id === user.id ? (
|
||||
<span className="tip-text">{t("common.yourself")}</span>
|
||||
|
@ -1,58 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "../../store";
|
||||
import { userService } from "../../services";
|
||||
import { validate, ValidatorConfig } from "../../helpers/validator";
|
||||
import toastHelper from "../Toast";
|
||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||
import showChangePasswordDialog from "../ChangePasswordDialog";
|
||||
import showUpdateAccountDialog from "../UpdateAccountDialog";
|
||||
import "../../less/settings/my-account-section.less";
|
||||
|
||||
const validateConfig: ValidatorConfig = {
|
||||
minLength: 1,
|
||||
maxLength: 24,
|
||||
noSpace: true,
|
||||
noChinese: false,
|
||||
};
|
||||
|
||||
const MyAccountSection = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
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 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 () => {
|
||||
showCommonDialog({
|
||||
title: "Reset Open API",
|
||||
@ -67,42 +25,23 @@ const MyAccountSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePreventDefault = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-container account-section-container">
|
||||
<p className="title-text">{t("setting.account-section.title")}</p>
|
||||
<label className="form-label">
|
||||
<span className="normal-text">{t("common.email")}:</span>
|
||||
<span className="normal-text">{user.email}</span>
|
||||
</label>
|
||||
<label className="form-label input-form-label username-label">
|
||||
<span className="normal-text">{t("common.username")}:</span>
|
||||
<input type="text" value={username} onChange={handleUsernameChanged} />
|
||||
<div className={`btns-container ${username === user.name ? "!hidden" : ""}`} onClick={handlePreventDefault}>
|
||||
<span className="btn confirm-btn" onClick={handleConfirmEditUsernameBtnClick}>
|
||||
{t("common.save")}
|
||||
</span>
|
||||
<span
|
||||
className="btn cancel-btn"
|
||||
onClick={() => {
|
||||
setUsername(user.name);
|
||||
}}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</span>
|
||||
<div className="flex flex-row justify-start items-end">
|
||||
<span className="text-2xl leading-10 font-medium">{user.nickname}</span>
|
||||
<span className="text-base ml-1 text-gray-500 leading-8">({user.username})</span>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center text-base text-gray-600">{user.email}</div>
|
||||
<div className="w-full flex flex-row justify-start items-center mt-2 space-x-2">
|
||||
<button className="px-2 py-1 border rounded-md text-sm hover:bg-gray-100" onClick={showUpdateAccountDialog}>
|
||||
Update Information
|
||||
</button>
|
||||
<button className="px-2 py-1 border rounded-md text-sm hover:bg-gray-100" onClick={showChangePasswordDialog}>
|
||||
Change Password
|
||||
</button>
|
||||
</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 className="section-container openapi-section-container">
|
||||
<p className="title-text">Open API</p>
|
||||
|
@ -118,7 +118,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
<div className="watermark-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">
|
||||
{createdDays} DAYS / {state.memoAmount} MEMOS
|
||||
</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) => (
|
||||
<div className="stat-wrapper" key={i}>
|
||||
<span className="stat-container null"></span>
|
||||
<span className="null"></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -28,10 +28,10 @@ const UserBanner = () => {
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
setUsername(owner.name);
|
||||
setUsername(owner.nickname || owner.username);
|
||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(owner.createdTs)) / 1000 / 3600 / 24));
|
||||
} else if (user) {
|
||||
setUsername(user.name);
|
||||
setUsername(user.nickname || user.username);
|
||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
|
||||
}
|
||||
}, [isVisitorMode, user, owner]);
|
||||
|
@ -13,3 +13,15 @@
|
||||
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);
|
||||
}
|
||||
|
||||
export function signin(email: string, password: string) {
|
||||
export function signin(username: string, password: string) {
|
||||
return axios.post<ResponseObject<User>>("/api/auth/signin", {
|
||||
email,
|
||||
username,
|
||||
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", {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
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;
|
||||
|
||||
> .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;
|
||||
max-height: 300px;
|
||||
@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;
|
||||
|
||||
&::placeholder {
|
||||
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",
|
||||
"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;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
|
||||
&::before {
|
||||
@apply font-bold mr-1;
|
||||
content: "•";
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply w-full my-1 p-3 rounded bg-gray-100 whitespace-pre-wrap;
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
> .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-text {
|
||||
@apply text-gray-400 text-sm;
|
||||
|
||||
&.username-field {
|
||||
@apply col-span-2 w-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +43,7 @@
|
||||
@apply font-mono text-gray-600;
|
||||
}
|
||||
|
||||
&.email-text {
|
||||
&.username-text {
|
||||
@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 {
|
||||
> .value-text {
|
||||
@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;
|
||||
height: 14px;
|
||||
|
||||
&.null {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
&.stat-day-l1-bg {
|
||||
@apply bg-green-400;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
"new-password": "New passworld",
|
||||
"repeat-new-password": "Repeat the new password",
|
||||
"username": "Username",
|
||||
"nickname": "Nickname",
|
||||
"save": "Save",
|
||||
"close": "Close",
|
||||
"cancel": "Cancel",
|
||||
|
@ -6,6 +6,7 @@
|
||||
"new-password": "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",
|
||||
"nickname": "Nickname",
|
||||
"save": "Lưu",
|
||||
"close": "Close",
|
||||
"cancel": "Hủy",
|
||||
|
@ -6,6 +6,7 @@
|
||||
"new-password": "新密码",
|
||||
"repeat-new-password": "重复新密码",
|
||||
"username": "用户名",
|
||||
"nickname": "昵称",
|
||||
"save": "保存",
|
||||
"close": "关闭",
|
||||
"cancel": "退出",
|
||||
|
@ -23,12 +23,12 @@ const Auth = () => {
|
||||
const systemStatus = useAppSelector((state) => state.global.systemStatus);
|
||||
const actionBtnLoadingState = useLoading(false);
|
||||
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 handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value as string;
|
||||
setEmail(text);
|
||||
setUsername(text);
|
||||
};
|
||||
|
||||
const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@ -41,9 +41,9 @@ const Auth = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const emailValidResult = validate(email, validateConfig);
|
||||
if (!emailValidResult.result) {
|
||||
toastHelper.error(t("common.email") + ": " + emailValidResult.reason);
|
||||
const usernameValidResult = validate(username, validateConfig);
|
||||
if (!usernameValidResult.result) {
|
||||
toastHelper.error(t("common.username") + ": " + usernameValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ const Auth = () => {
|
||||
|
||||
try {
|
||||
actionBtnLoadingState.setLoading();
|
||||
await api.signin(email, password);
|
||||
await api.signin(username, password);
|
||||
const user = await userService.doSignIn();
|
||||
if (user) {
|
||||
navigate("/");
|
||||
@ -74,9 +74,9 @@ const Auth = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const emailValidResult = validate(email, validateConfig);
|
||||
if (!emailValidResult.result) {
|
||||
toastHelper.error(t("common.email") + ": " + emailValidResult.reason);
|
||||
const usernameValidResult = validate(username, validateConfig);
|
||||
if (!usernameValidResult.result) {
|
||||
toastHelper.error(t("common.username") + ": " + usernameValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ const Auth = () => {
|
||||
|
||||
try {
|
||||
actionBtnLoadingState.setLoading();
|
||||
await api.signup(email, password, role);
|
||||
await api.signup(username, password, role);
|
||||
const user = await userService.doSignIn();
|
||||
if (user) {
|
||||
navigate("/");
|
||||
@ -118,8 +118,8 @@ const Auth = () => {
|
||||
</div>
|
||||
<div className={`page-content-container ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}>
|
||||
<div className="form-item-container input-form-container">
|
||||
<span className={`normal-text ${email ? "not-null" : ""}`}>{t("common.email")}</span>
|
||||
<input type="email" value={email} onChange={handleEmailInputChanged} required />
|
||||
<span className={`normal-text ${username ? "not-null" : ""}`}>{t("common.username")}</span>
|
||||
<input type="text" value={username} onChange={handleUsernameInputChanged} required />
|
||||
</div>
|
||||
<div className="form-item-container input-form-container">
|
||||
<span className={`normal-text ${password ? "not-null" : ""}`}>{t("common.password")}</span>
|
||||
|
@ -83,7 +83,7 @@ const Explore = () => {
|
||||
<div className="memo-header">
|
||||
<span className="time-text">{createdAtStr}</span>
|
||||
<a className="name-text" href={`/u/${memo.creator.id}`}>
|
||||
@{memo.creator.name}
|
||||
@{memo.creator.nickname || memo.creator.username}
|
||||
</a>
|
||||
</div>
|
||||
<MemoContent className="memo-content" content={memo.content} onMemoContentClick={() => undefined} />
|
||||
|
@ -83,7 +83,7 @@ const MemoDetail = () => {
|
||||
<div className="status-container">
|
||||
<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}`}>
|
||||
@{state.memo.creator.name}
|
||||
@{state.memo.creator.nickname || state.memo.creator.username}
|
||||
</a>
|
||||
</div>
|
||||
<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;
|
||||
rowStatus: RowStatus;
|
||||
|
||||
username: string;
|
||||
role: UserRole;
|
||||
email: string;
|
||||
name: string;
|
||||
nickname: string;
|
||||
openId: string;
|
||||
userSettingList: UserSetting[];
|
||||
|
||||
@ -18,18 +19,17 @@ interface User {
|
||||
}
|
||||
|
||||
interface UserCreate {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
}
|
||||
|
||||
interface UserPatch {
|
||||
id: UserId;
|
||||
|
||||
rowStatus?: RowStatus;
|
||||
|
||||
name?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
nickname?: string;
|
||||
password?: string;
|
||||
resetOpenId?: boolean;
|
||||
}
|
||||
|
Reference in New Issue
Block a user