chore: remove username field from user table

This commit is contained in:
Steven 2023-06-23 01:01:22 +08:00
parent 8dfae8a6aa
commit 2aae515544
11 changed files with 52 additions and 78 deletions

View File

@ -21,12 +21,12 @@ func getUserIDContextKey() string {
} }
type SignInRequest struct { type SignInRequest struct {
Username string `json:"username"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
} }
type SignUpRequest struct { type SignUpRequest struct {
Username string `json:"username"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
} }
@ -39,20 +39,20 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
} }
user, err := s.Store.GetUser(ctx, &store.FindUser{ user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &signin.Username, Email: &signin.Email,
}) })
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by username %s", signin.Username)).SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
} }
if user == nil { if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with username %s", signin.Username)) return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", signin.Email))
} else if user.RowStatus == store.Archived { } else if user.RowStatus == store.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username)) return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", signin.Email))
} }
// Compare the stored hashed password, with the hashed version of the password that was received. // Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Unmatched username and password").SetInternal(err) return echo.NewHTTPError(http.StatusUnauthorized, "Unmatched email and password").SetInternal(err)
} }
if err := auth.GenerateTokensAndSetCookies(c, user, secret); err != nil { if err := auth.GenerateTokensAndSetCookies(c, user, secret); err != nil {
@ -74,8 +74,8 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
} }
create := &store.User{ create := &store.User{
Username: signup.Username, Email: signup.Email,
Nickname: signup.Username, Nickname: signup.Email,
PasswordHash: string(passwordHash), PasswordHash: string(passwordHash),
} }
existingUsers, err := s.Store.ListUsers(ctx, &store.FindUser{}) existingUsers, err := s.Store.ListUsers(ctx, &store.FindUser{})

View File

@ -42,30 +42,25 @@ type User struct {
RowStatus RowStatus `json:"rowStatus"` RowStatus RowStatus `json:"rowStatus"`
// Domain specific fields // Domain specific fields
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"` Email string `json:"email"`
Nickname string `json:"nickname"`
Role Role `json:"role"` Role Role `json:"role"`
} }
type CreateUserRequest struct { type CreateUserRequest struct {
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"` Email string `json:"email"`
Nickname string `json:"nickname"`
Password string `json:"password"` Password string `json:"password"`
Role Role `json:"-"` Role Role `json:"-"`
} }
func (create CreateUserRequest) Validate() error { func (create CreateUserRequest) Validate() error {
if len(create.Username) < 3 {
return fmt.Errorf("username is too short, minimum length is 3")
}
if create.Nickname != "" && len(create.Nickname) < 3 {
return fmt.Errorf("username is too short, minimum length is 3")
}
if create.Email != "" && !validateEmail(create.Email) { if create.Email != "" && !validateEmail(create.Email) {
return fmt.Errorf("invalid email format") return fmt.Errorf("invalid email format")
} }
if create.Nickname != "" && len(create.Nickname) < 3 {
return fmt.Errorf("nickname is too short, minimum length is 3")
}
if len(create.Password) < 3 { if len(create.Password) < 3 {
return fmt.Errorf("password is too short, minimum length is 3") return fmt.Errorf("password is too short, minimum length is 3")
} }
@ -228,9 +223,8 @@ func convertUserFromStore(user *store.User) *User {
CreatedTs: user.CreatedTs, CreatedTs: user.CreatedTs,
UpdatedTs: user.UpdatedTs, UpdatedTs: user.UpdatedTs,
RowStatus: RowStatus(user.RowStatus), RowStatus: RowStatus(user.RowStatus),
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email, Email: user.Email,
Nickname: user.Nickname,
Role: Role(user.Role), Role: Role(user.Role),
} }
} }

View File

@ -46,26 +46,26 @@ type claimsMessage struct {
} }
// GenerateAPIToken generates an API token. // GenerateAPIToken generates an API token.
func GenerateAPIToken(userName string, userID int, secret string) (string, error) { func GenerateAPIToken(username string, userID int, secret string) (string, error) {
expirationTime := time.Now().Add(apiTokenDuration) expirationTime := time.Now().Add(apiTokenDuration)
return generateToken(userName, userID, AccessTokenAudienceName, expirationTime, []byte(secret)) return generateToken(username, userID, AccessTokenAudienceName, expirationTime, []byte(secret))
} }
// GenerateAccessToken generates an access token for web. // GenerateAccessToken generates an access token for web.
func GenerateAccessToken(userName string, userID int, secret string) (string, error) { func GenerateAccessToken(username string, userID int, secret string) (string, error) {
expirationTime := time.Now().Add(accessTokenDuration) expirationTime := time.Now().Add(accessTokenDuration)
return generateToken(userName, userID, AccessTokenAudienceName, expirationTime, []byte(secret)) return generateToken(username, userID, AccessTokenAudienceName, expirationTime, []byte(secret))
} }
// GenerateRefreshToken generates a refresh token for web. // GenerateRefreshToken generates a refresh token for web.
func GenerateRefreshToken(userName string, userID int, secret string) (string, error) { func GenerateRefreshToken(username string, userID int, secret string) (string, error) {
expirationTime := time.Now().Add(refreshTokenDuration) expirationTime := time.Now().Add(refreshTokenDuration)
return generateToken(userName, userID, RefreshTokenAudienceName, expirationTime, []byte(secret)) return generateToken(username, userID, RefreshTokenAudienceName, expirationTime, []byte(secret))
} }
// GenerateTokensAndSetCookies generates jwt token and saves it to the http-only cookie. // GenerateTokensAndSetCookies generates jwt token and saves it to the http-only cookie.
func GenerateTokensAndSetCookies(c echo.Context, user *store.User, secret string) error { func GenerateTokensAndSetCookies(c echo.Context, user *store.User, secret string) error {
accessToken, err := GenerateAccessToken(user.Username, user.ID, secret) accessToken, err := GenerateAccessToken(user.Email, user.ID, secret)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate access token") return errors.Wrap(err, "failed to generate access token")
} }
@ -74,7 +74,7 @@ func GenerateTokensAndSetCookies(c echo.Context, user *store.User, secret string
setTokenCookie(c, AccessTokenCookieName, accessToken, cookieExp) setTokenCookie(c, AccessTokenCookieName, accessToken, cookieExp)
// We generate here a new refresh token and saving it to the cookie. // We generate here a new refresh token and saving it to the cookie.
refreshToken, err := GenerateRefreshToken(user.Username, user.ID, secret) refreshToken, err := GenerateRefreshToken(user.Email, user.ID, secret)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate refresh token") return errors.Wrap(err, "failed to generate refresh token")
} }

View File

@ -16,9 +16,8 @@ CREATE TABLE user (
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
nickname TEXT NOT NULL, nickname TEXT NOT NULL,
email TEXT NOT NULL,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER' role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER'
); );

View File

@ -16,9 +16,8 @@ CREATE TABLE user (
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
nickname TEXT NOT NULL, nickname TEXT NOT NULL,
email TEXT NOT NULL,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER' role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER'
); );

View File

@ -26,9 +26,8 @@ type User struct {
RowStatus RowStatus RowStatus RowStatus
// Domain specific fields // Domain specific fields
Username string
Nickname string
Email string Email string
Nickname string
PasswordHash string PasswordHash string
Role Role Role Role
} }
@ -37,9 +36,8 @@ type UpdateUser struct {
ID int ID int
RowStatus *RowStatus RowStatus *RowStatus
Username *string
Nickname *string
Email *string Email *string
Nickname *string
PasswordHash *string PasswordHash *string
Role *Role Role *Role
} }
@ -47,9 +45,8 @@ type UpdateUser struct {
type FindUser struct { type FindUser struct {
ID *int ID *int
RowStatus *RowStatus RowStatus *RowStatus
Username *string
Nickname *string
Email *string Email *string
Nickname *string
Role *Role Role *Role
} }
@ -66,19 +63,17 @@ func (s *Store) CreateUser(ctx context.Context, create *User) (*User, error) {
query := ` query := `
INSERT INTO user ( INSERT INTO user (
username,
nickname,
email, email,
nickname,
password_hash, password_hash,
role role
) )
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?)
RETURNING id, created_ts, updated_ts, row_status RETURNING id, created_ts, updated_ts, row_status
` `
if err := tx.QueryRowContext(ctx, query, if err := tx.QueryRowContext(ctx, query,
create.Username,
create.Nickname,
create.Email, create.Email,
create.Nickname,
create.PasswordHash, create.PasswordHash,
create.Role, create.Role,
).Scan( ).Scan(
@ -110,15 +105,12 @@ func (s *Store) UpdateUser(ctx context.Context, update *UpdateUser) (*User, erro
if v := update.RowStatus; v != nil { if v := update.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v) set, args = append(set, "row_status = ?"), append(args, *v)
} }
if v := update.Username; v != nil { if v := update.Email; v != nil {
set, args = append(set, "username = ?"), append(args, *v) set, args = append(set, "email = ?"), append(args, *v)
} }
if v := update.Nickname; v != nil { if v := update.Nickname; v != nil {
set, args = append(set, "nickname = ?"), append(args, *v) set, args = append(set, "nickname = ?"), append(args, *v)
} }
if v := update.Email; v != nil {
set, args = append(set, "email = ?"), append(args, *v)
}
if v := update.PasswordHash; v != nil { if v := update.PasswordHash; v != nil {
set, args = append(set, "password_hash = ?"), append(args, *v) set, args = append(set, "password_hash = ?"), append(args, *v)
} }
@ -134,7 +126,7 @@ func (s *Store) UpdateUser(ctx context.Context, update *UpdateUser) (*User, erro
UPDATE user UPDATE user
SET ` + strings.Join(set, ", ") + ` SET ` + strings.Join(set, ", ") + `
WHERE id = ? WHERE id = ?
RETURNING id, created_ts, updated_ts, row_status, username, nickname, email, password_hash, role RETURNING id, created_ts, updated_ts, row_status, email, nickname, password_hash, role
` `
args = append(args, update.ID) args = append(args, update.ID)
user := &User{} user := &User{}
@ -143,9 +135,8 @@ func (s *Store) UpdateUser(ctx context.Context, update *UpdateUser) (*User, erro
&user.CreatedTs, &user.CreatedTs,
&user.UpdatedTs, &user.UpdatedTs,
&user.RowStatus, &user.RowStatus,
&user.Username,
&user.Nickname,
&user.Email, &user.Email,
&user.Nickname,
&user.PasswordHash, &user.PasswordHash,
&user.Role, &user.Role,
); err != nil { ); err != nil {
@ -236,15 +227,12 @@ func listUsers(ctx context.Context, tx *sql.Tx, find *FindUser) ([]*User, error)
if v := find.RowStatus; v != nil { if v := find.RowStatus; v != nil {
where, args = append(where, "row_status = ?"), append(args, v.String()) where, args = append(where, "row_status = ?"), append(args, v.String())
} }
if v := find.Username; v != nil { if v := find.Email; v != nil {
where, args = append(where, "username = ?"), append(args, *v) where, args = append(where, "email = ?"), append(args, *v)
} }
if v := find.Nickname; v != nil { if v := find.Nickname; v != nil {
where, args = append(where, "nickname = ?"), append(args, *v) where, args = append(where, "nickname = ?"), append(args, *v)
} }
if v := find.Email; v != nil {
where, args = append(where, "email = ?"), append(args, *v)
}
if v := find.Role; v != nil { if v := find.Role; v != nil {
where, args = append(where, "role = ?"), append(args, *v) where, args = append(where, "role = ?"), append(args, *v)
} }
@ -255,9 +243,8 @@ func listUsers(ctx context.Context, tx *sql.Tx, find *FindUser) ([]*User, error)
created_ts, created_ts,
updated_ts, updated_ts,
row_status, row_status,
username,
nickname,
email, email,
nickname,
password_hash, password_hash,
role role
FROM user FROM user
@ -278,9 +265,8 @@ func listUsers(ctx context.Context, tx *sql.Tx, find *FindUser) ([]*User, error)
&user.CreatedTs, &user.CreatedTs,
&user.UpdatedTs, &user.UpdatedTs,
&user.RowStatus, &user.RowStatus,
&user.Username,
&user.Nickname,
&user.Email, &user.Email,
&user.Nickname,
&user.PasswordHash, &user.PasswordHash,
&user.Role, &user.Role,
); err != nil { ); err != nil {

View File

@ -40,9 +40,8 @@ func TestUserStore(t *testing.T) {
func createTestingAdminUser(ctx context.Context, ts *store.Store) (*store.User, error) { func createTestingAdminUser(ctx context.Context, ts *store.Store) (*store.User, error) {
userCreate := &store.User{ userCreate := &store.User{
Role: store.RoleAdmin, Role: store.RoleAdmin,
Username: "test",
Nickname: "test_nickname",
Email: "test@test.com", Email: "test@test.com",
Nickname: "test_nickname",
} }
passwordHash, err := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.DefaultCost) passwordHash, err := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.DefaultCost)
if err != nil { if err != nil {

View File

@ -4,16 +4,16 @@ export function getSystemStatus() {
return axios.get<SystemStatus>("/api/v1/status"); return axios.get<SystemStatus>("/api/v1/status");
} }
export function signin(username: string, password: string) { export function signin(email: string, password: string) {
return axios.post<User>("/api/v1/auth/signin", { return axios.post<User>("/api/v1/auth/signin", {
username, email,
password, password,
}); });
} }
export function signup(username: string, password: string) { export function signup(email: string, password: string) {
return axios.post<User>("/api/v1/auth/signup", { return axios.post<User>("/api/v1/auth/signup", {
username, email,
password, password,
}); });
} }

View File

@ -9,7 +9,7 @@ import Icon from "../components/Icon";
const Auth: React.FC = () => { const Auth: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [username, setUsername] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const actionBtnLoadingState = useLoading(false); const actionBtnLoadingState = useLoading(false);
@ -20,9 +20,9 @@ const Auth: React.FC = () => {
} }
}, []); }, []);
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string; const text = e.target.value as string;
setUsername(text); setEmail(text);
}; };
const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -37,7 +37,7 @@ const Auth: React.FC = () => {
try { try {
actionBtnLoadingState.setLoading(); actionBtnLoadingState.setLoading();
await api.signin(username, password); await api.signin(email, password);
const user = await userService.doSignIn(); const user = await userService.doSignIn();
if (user) { if (user) {
navigate("/", { navigate("/", {
@ -60,7 +60,7 @@ const Auth: React.FC = () => {
try { try {
actionBtnLoadingState.setLoading(); actionBtnLoadingState.setLoading();
await api.signup(username, password); await api.signup(email, password);
const user = await userService.doSignIn(); const user = await userService.doSignIn();
if (user) { if (user) {
navigate("/", { navigate("/", {
@ -89,8 +89,8 @@ const Auth: React.FC = () => {
</div> </div>
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading ? "opacity-80" : ""}`}> <div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading ? "opacity-80" : ""}`}>
<div className="w-full flex flex-col mb-2"> <div className="w-full flex flex-col mb-2">
<span className="leading-8 mb-1 text-gray-600">Username</span> <span className="leading-8 mb-1 text-gray-600">Email</span>
<Input className="w-full py-3" type="username" value={username} onChange={handleUsernameInputChanged} /> <Input className="w-full py-3" type="email" value={email} onChange={handleEmailInputChanged} />
</div> </div>
<div className="w-full flex flex-col mb-2"> <div className="w-full flex flex-col mb-2">
<span className="leading-8 text-gray-600">Password</span> <span className="leading-8 text-gray-600">Password</span>

View File

@ -16,13 +16,11 @@ const UserDetail: React.FC = () => {
showChangePasswordDialog: false, showChangePasswordDialog: false,
}); });
console.log("here");
useEffect(() => { useEffect(() => {
if (!userService.getState().user) { if (!userService.getState().user) {
navigate("/user/auth"); navigate("/user/auth");
return; return;
} }
console.log("here");
}, []); }, []);
const handleChangePasswordBtnClick = async () => { const handleChangePasswordBtnClick = async () => {

View File

@ -9,9 +9,8 @@ interface User {
updatedTs: TimeStamp; updatedTs: TimeStamp;
rowStatus: RowStatus; rowStatus: RowStatus;
username: string;
nickname: string;
email: string; email: string;
nickname: string;
role: Role; role: Role;
} }