diff --git a/api/auth.go b/api/auth.go index ddb7998..c44a558 100644 --- a/api/auth.go +++ b/api/auth.go @@ -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"` } diff --git a/api/user.go b/api/user.go index 8360051..4e6afec 100644 --- a/api/user.go +++ b/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,10 +92,11 @@ type UserFind struct { RowStatus *RowStatus `json:"rowStatus"` // Domain specific fields - Email *string `json:"email"` - Role *Role - Name *string `json:"name"` - OpenID *string + Username *string `json:"username"` + Role *Role + Email *string `json:"email"` + Nickname *string `json:"nickname"` + OpenID *string } type UserDelete struct { diff --git a/server/acl.go b/server/acl.go index 5823432..ccb67e8 100644 --- a/server/acl.go +++ b/server/acl.go @@ -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) } diff --git a/server/auth.go b/server/auth.go index f6bc33c..15ea385 100644 --- a/server/auth.go +++ b/server/auth.go @@ -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(), } diff --git a/server/rss.go b/server/rss.go index b26d3ef..0540b9a 100644 --- a/server/rss.go +++ b/server/rss.go @@ -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), diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index 645a753..73b48e1 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -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 ); diff --git a/store/db/migration/prod/0.8/01__user_username.sql b/store/db/migration/prod/0.8/01__user_username.sql new file mode 100644 index 0000000..e23f638 --- /dev/null +++ b/store/db/migration/prod/0.8/01__user_username.sql @@ -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; diff --git a/store/db/migration/prod/0.8/01__memo_relation.sql b/store/db/migration/prod/0.8/02__memo_relation.sql similarity index 100% rename from store/db/migration/prod/0.8/01__memo_relation.sql rename to store/db/migration/prod/0.8/02__memo_relation.sql diff --git a/store/db/seed/10001__user.sql b/store/db/seed/10001__user.sql index 1d6182a..5dd6f6c 100644 --- a/store/db/seed/10001__user.sql +++ b/store/db/seed/10001__user.sql @@ -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 diff --git a/store/user.go b/store/user.go index e18d656..5d142d4 100644 --- a/store/user.go +++ b/store/user.go @@ -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,38 +263,25 @@ 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 { + var userRaw userRaw + 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) } - defer row.Close() - if row.Next() { - 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)} + return &userRaw, nil } 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, diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index 4e62a39..135f6ec 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -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 = ({ destroy }: Props) => { return ( <> -
+

{t("setting.account-section.change-password")}

- - -
- +

{t("common.new-password")}

+ +

{t("common.repeat-new-password")}

+ +
+ {t("common.cancel")} - + {t("common.save")}
diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx index ad4980d..dd79de0 100644 --- a/web/src/components/Settings/MemberSection.tsx +++ b/web/src/components/Settings/MemberSection.tsx @@ -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({ - createUserEmail: "", + createUserUsername: "", createUserPassword: "", }); const [userList, setUserList] = useState([]); @@ -31,10 +31,10 @@ const PreferencesSection = () => { setUserList(data); }; - const handleEmailInputChange = (event: React.ChangeEvent) => { + const handleUsernameInputChange = (event: React.ChangeEvent) => { 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 = () => {

{t("setting.member-section.create-a-member")}

- {t("common.email")} - + {t("common.username")} +
{t("common.password")} @@ -127,13 +126,13 @@ const PreferencesSection = () => {

{t("setting.member-list")}

ID - {t("common.email")} + {t("common.username")}
{userList.map((user) => (
{user.id} - {user.email} + {user.username}
{currentUser?.id === user.id ? ( {t("common.yourself")} diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx index 74f1469..4955a46 100644 --- a/web/src/components/Settings/MyAccountSection.tsx +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -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(user.name); const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`; - const handleUsernameChanged = (e: React.ChangeEvent) => { - 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 ( <>

{t("setting.account-section.title")}

- - - +
+ {user.nickname} + ({user.username}) +
+
{user.email}
+
+ + +

Open API

diff --git a/web/src/components/ShareMemoImageDialog.tsx b/web/src/components/ShareMemoImageDialog.tsx index fe3d87b..35a8331 100644 --- a/web/src/components/ShareMemoImageDialog.tsx +++ b/web/src/components/ShareMemoImageDialog.tsx @@ -118,7 +118,7 @@ const ShareMemoImageDialog: React.FC = (props: Props) => {
- {user.name} + {user.nickname || user.username} {createdDays} DAYS / {state.memoAmount} MEMOS diff --git a/web/src/components/UpdateAccountDialog.tsx b/web/src/components/UpdateAccountDialog.tsx new file mode 100644 index 0000000..bee6de7 --- /dev/null +++ b/web/src/components/UpdateAccountDialog.tsx @@ -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 = ({ destroy }: Props) => { + const { t } = useTranslation(); + const user = useAppSelector((state) => state.user.user as User); + const [state, setState] = useState({ + username: user.username, + nickname: user.nickname, + email: user.email, + }); + + useEffect(() => { + // do nth + }, []); + + const handleCloseBtnClick = () => { + destroy(); + }; + + const handleNicknameChanged = (e: React.ChangeEvent) => { + setState((state) => { + return { + ...state, + nickname: e.target.value as string, + }; + }); + }; + const handleUsernameChanged = (e: React.ChangeEvent) => { + setState((state) => { + return { + ...state, + username: e.target.value as string, + }; + }); + }; + const handleEmailChanged = (e: React.ChangeEvent) => { + 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 ( + <> +
+

Update information

+ +
+
+

Nickname

+ +

Username

+ +

Email

+ +
+ + {t("common.cancel")} + + + {t("common.save")} + +
+
+ + ); +}; + +function showUpdateAccountDialog() { + generateDialog( + { + className: "update-account-dialog", + }, + UpdateAccountDialog + ); +} + +export default showUpdateAccountDialog; diff --git a/web/src/components/UsageHeatMap.tsx b/web/src/components/UsageHeatMap.tsx index 35ae752..4023910 100644 --- a/web/src/components/UsageHeatMap.tsx +++ b/web/src/components/UsageHeatMap.tsx @@ -123,7 +123,7 @@ const UsageHeatMap = () => { })} {nullCell.map((_, i) => (
- +
))}
diff --git a/web/src/components/UserBanner.tsx b/web/src/components/UserBanner.tsx index 755a663..6b6a928 100644 --- a/web/src/components/UserBanner.tsx +++ b/web/src/components/UserBanner.tsx @@ -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]); diff --git a/web/src/css/tailwind.css b/web/src/css/tailwind.css index c08b2c9..b80f077 100644 --- a/web/src/css/tailwind.css +++ b/web/src/css/tailwind.css @@ -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; +} diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 499cb4c..822f604 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -14,19 +14,18 @@ export function upsertSystemSetting(systemSetting: SystemSetting) { return axios.post>("/api/system/setting", systemSetting); } -export function signin(email: string, password: string) { +export function signin(username: string, password: string) { return axios.post>("/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>("/api/auth/signup", { - email, + username, password, role, - name: email, }); } diff --git a/web/src/less/change-password-dialog.less b/web/src/less/change-password-dialog.less deleted file mode 100644 index 232ffdf..0000000 --- a/web/src/less/change-password-dialog.less +++ /dev/null @@ -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; - } - } - } - } - } -} diff --git a/web/src/less/editor.less b/web/src/less/editor.less index fbf5749..2e2e91f 100644 --- a/web/src/less/editor.less +++ b/web/src/less/editor.less @@ -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; diff --git a/web/src/less/global.less b/web/src/less/global.less index 8d9085b..6d461ba 100644 --- a/web/src/less/global.less +++ b/web/src/less/global.less @@ -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; -} diff --git a/web/src/less/memo-content.less b/web/src/less/memo-content.less index da32bc2..ebac5a1 100644 --- a/web/src/less/memo-content.less +++ b/web/src/less/memo-content.less @@ -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; diff --git a/web/src/less/search-bar.less b/web/src/less/search-bar.less index fe8f77a..bdbdedf 100644 --- a/web/src/less/search-bar.less +++ b/web/src/less/search-bar.less @@ -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; } } diff --git a/web/src/less/settings/member-section.less b/web/src/less/settings/member-section.less index 206284e..ee4f58a 100644 --- a/web/src/less/settings/member-section.less +++ b/web/src/less/settings/member-section.less @@ -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; } } diff --git a/web/src/less/settings/my-account-section.less b/web/src/less/settings/my-account-section.less index 23d377e..5d6a25e 100644 --- a/web/src/less/settings/my-account-section.less +++ b/web/src/less/settings/my-account-section.less @@ -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; diff --git a/web/src/less/usage-heat-map.less b/web/src/less/usage-heat-map.less index e60ee05..795bbe9 100644 --- a/web/src/less/usage-heat-map.less +++ b/web/src/less/usage-heat-map.less @@ -22,10 +22,6 @@ width: 14px; height: 14px; - &.null { - @apply bg-gray-200; - } - &.stat-day-l1-bg { @apply bg-green-400; } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index e3efe56..845ef22 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -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", diff --git a/web/src/locales/vi.json b/web/src/locales/vi.json index e99fbc6..f6e3f4a 100644 --- a/web/src/locales/vi.json +++ b/web/src/locales/vi.json @@ -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", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 55a10c4..8c2917d 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -6,6 +6,7 @@ "new-password": "新密码", "repeat-new-password": "重复新密码", "username": "用户名", + "nickname": "昵称", "save": "保存", "close": "关闭", "cancel": "退出", diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index d49f9c2..62a451d 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -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) => { + const handleUsernameInputChanged = (e: React.ChangeEvent) => { const text = e.target.value as string; - setEmail(text); + setUsername(text); }; const handlePasswordInputChanged = (e: React.ChangeEvent) => { @@ -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 = () => {
- {t("common.email")} - + {t("common.username")} +
{t("common.password")} diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 235c226..0f641ed 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -83,7 +83,7 @@ const Explore = () => { undefined} /> diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 2323e0a..ae8535b 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -83,7 +83,7 @@ const MemoDetail = () => {
{dayjs(state.memo.displayTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss")} - @{state.memo.creator.name} + @{state.memo.creator.nickname || state.memo.creator.username}