2023-06-17 17:35:17 +03:00
package v1
2023-07-02 13:56:25 +03:00
import (
"encoding/json"
"fmt"
"net/http"
2023-09-18 17:37:13 +03:00
"strings"
2023-07-02 13:56:25 +03:00
"time"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
2023-09-17 17:55:13 +03:00
"golang.org/x/crypto/bcrypt"
2023-07-06 17:53:38 +03:00
"github.com/usememos/memos/common/util"
2023-07-02 13:56:25 +03:00
"github.com/usememos/memos/store"
)
2023-06-17 17:35:17 +03:00
// Role is the type of a role.
type Role string
const (
2023-07-02 13:56:25 +03:00
// RoleHost is the HOST role.
RoleHost Role = "HOST"
// RoleAdmin is the ADMIN role.
RoleAdmin Role = "ADMIN"
// RoleUser is the USER role.
RoleUser Role = "USER"
2023-06-17 17:35:17 +03:00
)
2023-07-02 13:56:25 +03:00
func ( role Role ) String ( ) string {
return string ( role )
}
type User struct {
2023-08-04 16:55:07 +03:00
ID int32 ` json:"id" `
2023-07-02 13:56:25 +03:00
// Standard fields
RowStatus RowStatus ` json:"rowStatus" `
CreatedTs int64 ` json:"createdTs" `
UpdatedTs int64 ` json:"updatedTs" `
// Domain specific fields
Username string ` json:"username" `
Role Role ` json:"role" `
Email string ` json:"email" `
Nickname string ` json:"nickname" `
PasswordHash string ` json:"-" `
AvatarURL string ` json:"avatarUrl" `
UserSettingList [ ] * UserSetting ` json:"userSettingList" `
}
type CreateUserRequest struct {
Username string ` json:"username" `
Role Role ` json:"role" `
Email string ` json:"email" `
Nickname string ` json:"nickname" `
Password string ` json:"password" `
}
2023-08-09 16:53:06 +03:00
type UpdateUserRequest struct {
2023-09-14 17:57:27 +03:00
RowStatus * RowStatus ` json:"rowStatus" `
Username * string ` json:"username" `
Email * string ` json:"email" `
Nickname * string ` json:"nickname" `
Password * string ` json:"password" `
AvatarURL * string ` json:"avatarUrl" `
2023-08-09 16:53:06 +03:00
}
func ( s * APIV1Service ) registerUserRoutes ( g * echo . Group ) {
2023-08-09 17:30:27 +03:00
g . GET ( "/user" , s . GetUserList )
g . POST ( "/user" , s . CreateUser )
g . GET ( "/user/me" , s . GetCurrentUser )
2023-08-09 16:53:06 +03:00
// NOTE: This should be moved to /api/v2/user/:username
2023-08-09 17:30:27 +03:00
g . GET ( "/user/name/:username" , s . GetUserByUsername )
g . GET ( "/user/:id" , s . GetUserByID )
g . PATCH ( "/user/:id" , s . UpdateUser )
g . DELETE ( "/user/:id" , s . DeleteUser )
2023-08-09 16:53:06 +03:00
}
2023-08-09 17:30:27 +03:00
// GetUserList godoc
2023-08-09 16:53:06 +03:00
//
// @Summary Get a list of users
// @Tags user
// @Produce json
// @Success 200 {object} []store.User "User list"
// @Failure 500 {object} nil "Failed to fetch user list"
// @Router /api/v1/user [GET]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) GetUserList ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
list , err := s . Store . ListUsers ( ctx , & store . FindUser { } )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to fetch user list" ) . SetInternal ( err )
}
userMessageList := make ( [ ] * User , 0 , len ( list ) )
for _ , user := range list {
userMessage := convertUserFromStore ( user )
// data desensitize
userMessage . Email = ""
userMessageList = append ( userMessageList , userMessage )
}
return c . JSON ( http . StatusOK , userMessageList )
}
2023-08-09 17:30:27 +03:00
// CreateUser godoc
2023-08-09 16:53:06 +03:00
//
// @Summary Create a user
// @Tags user
// @Accept json
// @Produce json
// @Param body body CreateUserRequest true "Request object"
// @Success 200 {object} store.User "Created user"
// @Failure 400 {object} nil "Malformatted post user request | Invalid user create format"
// @Failure 401 {object} nil "Missing auth session | Unauthorized to create user"
// @Failure 403 {object} nil "Could not create host user"
// @Failure 500 {object} nil "Failed to find user by id | Failed to generate password hash | Failed to create user | Failed to create activity"
// @Router /api/v1/user [POST]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) CreateUser ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
2023-09-14 15:16:17 +03:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 16:53:06 +03:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing auth session" )
}
currentUser , err := s . Store . GetUser ( ctx , & store . FindUser {
ID : & userID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user by id" ) . SetInternal ( err )
}
if currentUser == nil {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing auth session" )
}
if currentUser . Role != store . RoleHost {
return echo . NewHTTPError ( http . StatusUnauthorized , "Unauthorized to create user" )
}
userCreate := & CreateUserRequest { }
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( userCreate ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted post user request" ) . SetInternal ( err )
}
if err := userCreate . Validate ( ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Invalid user create format" ) . SetInternal ( err )
}
2023-09-18 17:37:13 +03:00
if ! usernameMatcher . MatchString ( strings . ToLower ( userCreate . Username ) ) {
2023-09-18 17:34:31 +03:00
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "Invalid username %s" , userCreate . Username ) ) . SetInternal ( err )
}
2023-08-09 16:53:06 +03:00
// Disallow host user to be created.
if userCreate . Role == RoleHost {
return echo . NewHTTPError ( http . StatusForbidden , "Could not create host user" )
}
passwordHash , err := bcrypt . GenerateFromPassword ( [ ] byte ( userCreate . Password ) , bcrypt . DefaultCost )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate password hash" ) . SetInternal ( err )
}
user , err := s . Store . CreateUser ( ctx , & store . User {
Username : userCreate . Username ,
Role : store . Role ( userCreate . Role ) ,
Email : userCreate . Email ,
Nickname : userCreate . Nickname ,
PasswordHash : string ( passwordHash ) ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create user" ) . SetInternal ( err )
}
userMessage := convertUserFromStore ( user )
if err := s . createUserCreateActivity ( c , userMessage ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create activity" ) . SetInternal ( err )
}
return c . JSON ( http . StatusOK , userMessage )
}
2023-08-09 17:30:27 +03:00
// GetCurrentUser godoc
2023-08-09 16:53:06 +03:00
//
// @Summary Get current user
// @Tags user
// @Produce json
// @Success 200 {object} store.User "Current user"
// @Failure 401 {object} nil "Missing auth session"
// @Failure 500 {object} nil "Failed to find user | Failed to find userSettingList"
// @Router /api/v1/user/me [GET]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) GetCurrentUser ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
2023-09-14 15:16:17 +03:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 16:53:06 +03:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing auth session" )
}
user , err := s . Store . GetUser ( ctx , & store . FindUser { ID : & userID } )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user" ) . SetInternal ( err )
}
if user == nil {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing auth session" )
}
list , err := s . Store . ListUserSettings ( ctx , & store . FindUserSetting {
UserID : & userID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find userSettingList" ) . SetInternal ( err )
}
userSettingList := [ ] * UserSetting { }
for _ , userSetting := range list {
userSettingList = append ( userSettingList , convertUserSettingFromStore ( userSetting ) )
}
userMessage := convertUserFromStore ( user )
userMessage . UserSettingList = userSettingList
return c . JSON ( http . StatusOK , userMessage )
}
2023-08-09 17:30:27 +03:00
// GetUserByUsername godoc
2023-08-09 16:53:06 +03:00
//
// @Summary Get user by username
// @Tags user
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} store.User "Requested user"
// @Failure 404 {object} nil "User not found"
// @Failure 500 {object} nil "Failed to find user"
// @Router /api/v1/user/name/{username} [GET]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) GetUserByUsername ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
username := c . Param ( "username" )
user , err := s . Store . GetUser ( ctx , & store . FindUser { Username : & username } )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user" ) . SetInternal ( err )
}
if user == nil {
return echo . NewHTTPError ( http . StatusNotFound , "User not found" )
}
userMessage := convertUserFromStore ( user )
// data desensitize
userMessage . Email = ""
return c . JSON ( http . StatusOK , userMessage )
}
2023-08-09 17:30:27 +03:00
// GetUserByID godoc
2023-08-09 16:53:06 +03:00
//
// @Summary Get user by id
// @Tags user
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} store.User "Requested user"
// @Failure 400 {object} nil "Malformatted user id"
// @Failure 404 {object} nil "User not found"
// @Failure 500 {object} nil "Failed to find user"
// @Router /api/v1/user/{id} [GET]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) GetUserByID ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
id , err := util . ConvertStringToInt32 ( c . Param ( "id" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted user id" ) . SetInternal ( err )
}
user , err := s . Store . GetUser ( ctx , & store . FindUser { ID : & id } )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user" ) . SetInternal ( err )
}
if user == nil {
return echo . NewHTTPError ( http . StatusNotFound , "User not found" )
}
userMessage := convertUserFromStore ( user )
// data desensitize
userMessage . Email = ""
return c . JSON ( http . StatusOK , userMessage )
}
2023-08-09 17:30:27 +03:00
// DeleteUser godoc
2023-08-09 16:53:06 +03:00
//
// @Summary Delete a user
// @Tags user
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {boolean} true "User deleted"
// @Failure 400 {object} nil "ID is not a number: %s | Current session user not found with ID: %d"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 403 {object} nil "Unauthorized to delete user"
// @Failure 500 {object} nil "Failed to find user | Failed to delete user"
// @Router /api/v1/user/{id} [DELETE]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) DeleteUser ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
2023-09-14 15:16:17 +03:00
currentUserID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 16:53:06 +03:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
currentUser , err := s . Store . GetUser ( ctx , & store . FindUser {
ID : & currentUserID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user" ) . SetInternal ( err )
}
if currentUser == nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "Current session user not found with ID: %d" , currentUserID ) ) . SetInternal ( err )
} else if currentUser . Role != store . RoleHost {
return echo . NewHTTPError ( http . StatusForbidden , "Unauthorized to delete user" ) . SetInternal ( err )
}
userID , err := util . ConvertStringToInt32 ( c . Param ( "id" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "ID is not a number: %s" , c . Param ( "id" ) ) ) . SetInternal ( err )
}
userDelete := & store . DeleteUser {
ID : userID ,
}
if err := s . Store . DeleteUser ( ctx , userDelete ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to delete user" ) . SetInternal ( err )
}
return c . JSON ( http . StatusOK , true )
}
2023-08-09 17:30:27 +03:00
// UpdateUser godoc
2023-08-09 16:53:06 +03:00
//
// @Summary Update a user
// @Tags user
// @Produce json
// @Param id path string true "User ID"
// @Param patch body UpdateUserRequest true "Patch request"
// @Success 200 {object} store.User "Updated user"
// @Failure 400 {object} nil "ID is not a number: %s | Current session user not found with ID: %d | Malformatted patch user request | Invalid update user request"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 403 {object} nil "Unauthorized to update user"
// @Failure 500 {object} nil "Failed to find user | Failed to generate password hash | Failed to patch user | Failed to find userSettingList"
// @Router /api/v1/user/{id} [PATCH]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) UpdateUser ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
userID , err := util . ConvertStringToInt32 ( c . Param ( "id" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "ID is not a number: %s" , c . Param ( "id" ) ) ) . SetInternal ( err )
}
2023-09-14 15:16:17 +03:00
currentUserID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 16:53:06 +03:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
currentUser , err := s . Store . GetUser ( ctx , & store . FindUser { ID : & currentUserID } )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user" ) . SetInternal ( err )
}
if currentUser == nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "Current session user not found with ID: %d" , currentUserID ) ) . SetInternal ( err )
} else if currentUser . Role != store . RoleHost && currentUserID != userID {
return echo . NewHTTPError ( http . StatusForbidden , "Unauthorized to update user" ) . SetInternal ( err )
}
request := & UpdateUserRequest { }
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( request ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted patch user request" ) . SetInternal ( err )
}
if err := request . Validate ( ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Invalid update user request" ) . SetInternal ( err )
}
currentTs := time . Now ( ) . Unix ( )
userUpdate := & store . UpdateUser {
ID : userID ,
UpdatedTs : & currentTs ,
}
if request . RowStatus != nil {
rowStatus := store . RowStatus ( request . RowStatus . String ( ) )
userUpdate . RowStatus = & rowStatus
}
if request . Username != nil {
2023-09-18 17:37:13 +03:00
if ! usernameMatcher . MatchString ( strings . ToLower ( * request . Username ) ) {
2023-09-18 17:34:31 +03:00
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "Invalid username %s" , * request . Username ) ) . SetInternal ( err )
}
2023-08-09 16:53:06 +03:00
userUpdate . Username = request . Username
}
if request . Email != nil {
userUpdate . Email = request . Email
}
if request . Nickname != nil {
userUpdate . Nickname = request . Nickname
}
if request . Password != nil {
passwordHash , err := bcrypt . GenerateFromPassword ( [ ] byte ( * request . Password ) , bcrypt . DefaultCost )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate password hash" ) . SetInternal ( err )
}
passwordHashStr := string ( passwordHash )
userUpdate . PasswordHash = & passwordHashStr
}
if request . AvatarURL != nil {
userUpdate . AvatarURL = request . AvatarURL
}
user , err := s . Store . UpdateUser ( ctx , userUpdate )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to patch user" ) . SetInternal ( err )
}
list , err := s . Store . ListUserSettings ( ctx , & store . FindUserSetting {
UserID : & userID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find userSettingList" ) . SetInternal ( err )
}
userSettingList := [ ] * UserSetting { }
for _ , userSetting := range list {
userSettingList = append ( userSettingList , convertUserSettingFromStore ( userSetting ) )
}
userMessage := convertUserFromStore ( user )
userMessage . UserSettingList = userSettingList
return c . JSON ( http . StatusOK , userMessage )
}
2023-07-02 13:56:25 +03:00
func ( create CreateUserRequest ) Validate ( ) error {
if len ( create . Username ) < 3 {
2023-09-29 08:04:54 +03:00
return errors . New ( "username is too short, minimum length is 3" )
2023-07-02 13:56:25 +03:00
}
if len ( create . Username ) > 32 {
2023-09-29 08:04:54 +03:00
return errors . New ( "username is too long, maximum length is 32" )
2023-07-02 13:56:25 +03:00
}
if len ( create . Password ) < 3 {
2023-09-29 08:04:54 +03:00
return errors . New ( "password is too short, minimum length is 3" )
2023-07-02 13:56:25 +03:00
}
if len ( create . Password ) > 512 {
2023-09-29 08:04:54 +03:00
return errors . New ( "password is too long, maximum length is 512" )
2023-07-02 13:56:25 +03:00
}
if len ( create . Nickname ) > 64 {
2023-09-29 08:04:54 +03:00
return errors . New ( "nickname is too long, maximum length is 64" )
2023-07-02 13:56:25 +03:00
}
if create . Email != "" {
if len ( create . Email ) > 256 {
2023-09-29 08:04:54 +03:00
return errors . New ( "email is too long, maximum length is 256" )
2023-07-02 13:56:25 +03:00
}
2023-07-06 17:53:38 +03:00
if ! util . ValidateEmail ( create . Email ) {
2023-09-29 08:04:54 +03:00
return errors . New ( "invalid email format" )
2023-07-02 13:56:25 +03:00
}
}
return nil
}
func ( update UpdateUserRequest ) Validate ( ) error {
if update . Username != nil && len ( * update . Username ) < 3 {
2023-09-29 08:04:54 +03:00
return errors . New ( "username is too short, minimum length is 3" )
2023-07-02 13:56:25 +03:00
}
if update . Username != nil && len ( * update . Username ) > 32 {
2023-09-29 08:04:54 +03:00
return errors . New ( "username is too long, maximum length is 32" )
2023-07-02 13:56:25 +03:00
}
if update . Password != nil && len ( * update . Password ) < 3 {
2023-09-29 08:04:54 +03:00
return errors . New ( "password is too short, minimum length is 3" )
2023-07-02 13:56:25 +03:00
}
if update . Password != nil && len ( * update . Password ) > 512 {
2023-09-29 08:04:54 +03:00
return errors . New ( "password is too long, maximum length is 512" )
2023-07-02 13:56:25 +03:00
}
if update . Nickname != nil && len ( * update . Nickname ) > 64 {
2023-09-29 08:04:54 +03:00
return errors . New ( "nickname is too long, maximum length is 64" )
2023-07-02 13:56:25 +03:00
}
if update . AvatarURL != nil {
if len ( * update . AvatarURL ) > 2 << 20 {
2023-09-29 08:04:54 +03:00
return errors . New ( "avatar is too large, maximum is 2MB" )
2023-07-02 13:56:25 +03:00
}
}
if update . Email != nil && * update . Email != "" {
if len ( * update . Email ) > 256 {
2023-09-29 08:04:54 +03:00
return errors . New ( "email is too long, maximum length is 256" )
2023-07-02 13:56:25 +03:00
}
2023-07-06 17:53:38 +03:00
if ! util . ValidateEmail ( * update . Email ) {
2023-09-29 08:04:54 +03:00
return errors . New ( "invalid email format" )
2023-07-02 13:56:25 +03:00
}
}
return nil
}
func ( s * APIV1Service ) createUserCreateActivity ( c echo . Context , user * User ) error {
ctx := c . Request ( ) . Context ( )
payload := ActivityUserCreatePayload {
UserID : user . ID ,
Username : user . Username ,
Role : user . Role ,
}
payloadBytes , err := json . Marshal ( payload )
if err != nil {
return errors . Wrap ( err , "failed to marshal activity payload" )
}
2023-07-06 16:56:42 +03:00
activity , err := s . Store . CreateActivity ( ctx , & store . Activity {
2023-07-02 13:56:25 +03:00
CreatorID : user . ID ,
Type : ActivityUserCreate . String ( ) ,
Level : ActivityInfo . String ( ) ,
Payload : string ( payloadBytes ) ,
} )
if err != nil || activity == nil {
return errors . Wrap ( err , "failed to create activity" )
}
return err
}
2023-07-09 16:13:26 +03:00
func convertUserFromStore ( user * store . User ) * User {
2023-07-02 13:56:25 +03:00
return & User {
ID : user . ID ,
RowStatus : RowStatus ( user . RowStatus ) ,
CreatedTs : user . CreatedTs ,
UpdatedTs : user . UpdatedTs ,
Username : user . Username ,
Role : Role ( user . Role ) ,
Email : user . Email ,
Nickname : user . Nickname ,
PasswordHash : user . PasswordHash ,
AvatarURL : user . AvatarURL ,
2023-06-17 17:35:17 +03:00
}
}