feat: add username field (#544)

* feat: add username field

* chore: update
pull/547/head
boojack 3 years ago committed by GitHub
parent a0667abec8
commit 2042737004
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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"`
}

@ -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.Email) < 6 {
return fmt.Errorf("email is too short, minimum length is 6")
if len(create.Username) < 4 {
return fmt.Errorf("username is too short, minimum length is 4")
}
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 {

@ -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
);

@ -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,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,

@ -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">
<input
type="password"
placeholder={t("common.repeat-new-password")}
value={newPasswordAgain}
onChange={handleNewPasswordAgainChanged}
/>
</label>
<div className="btns-container">
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
<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}
/>
<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>
</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 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>
</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>

@ -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

@ -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;
}

Loading…
Cancel
Save