2022-09-20 12:55:00 +02:00
package member
import (
2022-10-03 10:59:30 +02:00
"fmt"
2022-09-20 12:55:00 +02:00
"net/http"
2023-03-18 23:00:44 +01:00
"strings"
2022-09-20 12:55:00 +02:00
2023-04-02 22:50:22 +02:00
"codeberg.org/u1f320/pronouns.cc/backend/common"
2022-09-20 12:55:00 +02:00
"codeberg.org/u1f320/pronouns.cc/backend/db"
2022-10-03 10:59:30 +02:00
"codeberg.org/u1f320/pronouns.cc/backend/log"
2022-09-20 12:55:00 +02:00
"codeberg.org/u1f320/pronouns.cc/backend/server"
2022-10-03 10:59:30 +02:00
"emperror.dev/errors"
2022-09-20 12:55:00 +02:00
"github.com/go-chi/render"
)
type CreateMemberRequest struct {
2023-01-31 00:50:17 +01:00
Name string ` json:"name" `
DisplayName * string ` json:"display_name" `
Bio string ` json:"bio" `
Avatar string ` json:"avatar" `
Links [ ] string ` json:"links" `
Names [ ] db . FieldEntry ` json:"names" `
Pronouns [ ] db . PronounEntry ` json:"pronouns" `
Fields [ ] db . Field ` json:"fields" `
2022-09-20 12:55:00 +02:00
}
func ( s * Server ) createMember ( w http . ResponseWriter , r * http . Request ) ( err error ) {
ctx := r . Context ( )
2022-10-03 10:59:30 +02:00
claims , _ := server . ClaimsFromContext ( ctx )
2023-03-30 16:58:35 +02:00
if ! claims . TokenWrite {
return server . APIError { Code : server . ErrMissingPermissions , Details : "This token is read-only" }
}
2022-10-03 10:59:30 +02:00
u , err := s . DB . User ( ctx , claims . UserID )
if err != nil {
return errors . Wrap ( err , "getting user" )
}
memberCount , err := s . DB . MemberCount ( ctx , claims . UserID )
if err != nil {
return errors . Wrap ( err , "getting member count" )
}
if memberCount > db . MaxMemberCount {
return server . APIError {
Code : server . ErrMemberLimitReached ,
}
}
2022-09-20 12:55:00 +02:00
var cmr CreateMemberRequest
err = render . Decode ( r , & cmr )
if err != nil {
if _ , ok := err . ( server . APIError ) ; ok {
return err
}
return server . APIError { Code : server . ErrBadRequest }
}
2023-03-18 23:00:44 +01:00
// remove whitespace from all fields
cmr . Name = strings . TrimSpace ( cmr . Name )
cmr . Bio = strings . TrimSpace ( cmr . Bio )
if cmr . DisplayName != nil {
* cmr . DisplayName = strings . TrimSpace ( * cmr . DisplayName )
}
2022-10-03 10:59:30 +02:00
// validate everything
if cmr . Name == "" {
return server . APIError {
Code : server . ErrBadRequest ,
2022-11-20 21:09:29 +01:00
Details : "Name may not be empty" ,
}
} else if len ( cmr . Name ) > 100 {
return server . APIError {
Code : server . ErrBadRequest ,
Details : "Name may not be longer than 100 characters" ,
2022-10-03 10:59:30 +02:00
}
}
2023-03-18 23:00:44 +01:00
if ! db . MemberNameValid ( cmr . Name ) {
return server . APIError {
Code : server . ErrBadRequest ,
2023-05-12 01:39:02 +02:00
Details : "Member name cannot contain any of the following: @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , and cannot be one or two periods." ,
2023-03-18 23:00:44 +01:00
}
}
2023-04-02 22:50:22 +02:00
if common . StringLength ( & cmr . Name ) > db . MaxMemberNameLength {
return server . APIError {
Code : server . ErrBadRequest ,
Details : fmt . Sprintf ( "Name name too long (max %d, current %d)" , db . MaxMemberNameLength , common . StringLength ( & cmr . Name ) ) ,
}
}
if common . StringLength ( cmr . DisplayName ) > db . MaxDisplayNameLength {
return server . APIError {
Code : server . ErrBadRequest ,
Details : fmt . Sprintf ( "Display name too long (max %d, current %d)" , db . MaxDisplayNameLength , common . StringLength ( cmr . DisplayName ) ) ,
}
}
if common . StringLength ( & cmr . Bio ) > db . MaxUserBioLength {
return server . APIError {
Code : server . ErrBadRequest ,
Details : fmt . Sprintf ( "Bio too long (max %d, current %d)" , db . MaxUserBioLength , common . StringLength ( & cmr . Bio ) ) ,
}
}
2023-04-19 12:00:21 +02:00
if err := validateSlicePtr ( "name" , & cmr . Names , u . CustomPreferences ) ; err != nil {
2023-03-14 01:30:46 +01:00
return * err
2022-10-03 10:59:30 +02:00
}
2023-04-19 12:00:21 +02:00
if err := validateSlicePtr ( "pronoun" , & cmr . Pronouns , u . CustomPreferences ) ; err != nil {
2023-03-14 01:30:46 +01:00
return * err
2022-10-03 10:59:30 +02:00
}
2023-04-19 12:00:21 +02:00
if err := validateSlicePtr ( "field" , & cmr . Fields , u . CustomPreferences ) ; err != nil {
2023-03-14 01:30:46 +01:00
return * err
2022-10-03 10:59:30 +02:00
}
tx , err := s . DB . Begin ( ctx )
if err != nil {
return errors . Wrap ( err , "starting transaction" )
}
defer tx . Rollback ( ctx )
2022-11-20 21:09:29 +01:00
m , err := s . DB . CreateMember ( ctx , tx , claims . UserID , cmr . Name , cmr . DisplayName , cmr . Bio , cmr . Links )
2022-10-03 10:59:30 +02:00
if err != nil {
2022-11-20 21:09:29 +01:00
if errors . Cause ( err ) == db . ErrMemberNameInUse {
return server . APIError { Code : server . ErrMemberNameInUse }
}
2022-10-03 10:59:30 +02:00
return err
}
// set names, pronouns, fields
2023-04-08 01:25:27 +02:00
err = s . DB . SetMemberNamesPronouns ( ctx , tx , m . ID , db . NotNull ( cmr . Names ) , db . NotNull ( cmr . Pronouns ) )
2022-10-03 10:59:30 +02:00
if err != nil {
2023-01-31 00:50:17 +01:00
log . Errorf ( "setting names and pronouns for member %v: %v" , m . ID , err )
2022-10-03 10:59:30 +02:00
return err
}
2023-01-31 00:50:17 +01:00
m . Names = cmr . Names
m . Pronouns = cmr . Pronouns
2022-11-20 21:09:29 +01:00
err = s . DB . SetMemberFields ( ctx , tx , m . ID , cmr . Fields )
2022-10-03 10:59:30 +02:00
if err != nil {
2022-11-20 21:09:29 +01:00
log . Errorf ( "setting fields for member %v: %v" , m . ID , err )
2022-10-03 10:59:30 +02:00
return err
}
if cmr . Avatar != "" {
webp , jpg , err := s . DB . ConvertAvatar ( cmr . Avatar )
if err != nil {
if err == db . ErrInvalidDataURI {
return server . APIError {
Code : server . ErrBadRequest ,
Details : "invalid avatar data URI" ,
}
} else if err == db . ErrInvalidContentType {
return server . APIError {
Code : server . ErrBadRequest ,
Details : "invalid avatar content type" ,
}
}
log . Errorf ( "converting member avatar: %v" , err )
return err
}
2023-03-13 02:04:09 +01:00
hash , err := s . DB . WriteMemberAvatar ( ctx , m . ID , webp , jpg )
2022-10-03 10:59:30 +02:00
if err != nil {
log . Errorf ( "uploading member avatar: %v" , err )
return err
}
2023-03-13 02:04:09 +01:00
err = tx . QueryRow ( ctx , "UPDATE members SET avatar = $1 WHERE id = $2" , hash , m . ID ) . Scan ( & m . Avatar )
2022-10-03 10:59:30 +02:00
if err != nil {
return errors . Wrap ( err , "setting avatar urls in db" )
}
}
2023-05-02 02:54:08 +02:00
// update last active time
err = s . DB . UpdateActiveTime ( ctx , tx , claims . UserID )
if err != nil {
log . Errorf ( "updating last active time for user %v: %v" , claims . UserID , err )
return err
}
2022-10-03 10:59:30 +02:00
err = tx . Commit ( ctx )
if err != nil {
return errors . Wrap ( err , "committing transaction" )
}
2023-05-25 13:40:15 +02:00
render . JSON ( w , r , dbMemberToMember ( u , m , cmr . Fields , nil , true ) )
2022-10-03 10:59:30 +02:00
return nil
}
type validator interface {
2023-04-19 12:00:21 +02:00
Validate ( custom db . CustomPreferences ) string
2022-10-03 10:59:30 +02:00
}
// validateSlicePtr validates a slice of validators.
// If the slice is nil, a nil error is returned (assuming that the field is not required)
2023-04-19 12:00:21 +02:00
func validateSlicePtr [ T validator ] ( typ string , slice * [ ] T , custom db . CustomPreferences ) * server . APIError {
2022-10-03 10:59:30 +02:00
if slice == nil {
return nil
}
max := db . MaxFields
if typ != "field" {
max = db . FieldEntriesLimit
}
// max 25 fields
if len ( * slice ) > max {
return & server . APIError {
Code : server . ErrBadRequest ,
Details : fmt . Sprintf ( "Too many %ss (max %d, current %d)" , typ , max , len ( * slice ) ) ,
}
}
// validate all fields
for i , pronouns := range * slice {
2023-04-19 12:00:21 +02:00
if s := pronouns . Validate ( custom ) ; s != "" {
2022-10-03 10:59:30 +02:00
return & server . APIError {
Code : server . ErrBadRequest ,
2023-03-14 01:30:46 +01:00
Details : fmt . Sprintf ( "%s %d: %s" , typ , i + 1 , s ) ,
2022-10-03 10:59:30 +02:00
}
}
}
2022-09-20 12:55:00 +02:00
return nil
}