diff --git a/backend/db/member.go b/backend/db/member.go index 56b586e..04345b9 100644 --- a/backend/db/member.go +++ b/backend/db/member.go @@ -13,12 +13,13 @@ import ( const MaxMemberCount = 500 type Member struct { - ID xid.ID - UserID xid.ID - Name string - Bio *string - AvatarURLs []string `db:"avatar_urls"` - Links []string + ID xid.ID + UserID xid.ID + Name string + DisplayName *string + Bio *string + AvatarURLs []string `db:"avatar_urls"` + Links []string } const ( @@ -79,10 +80,10 @@ func (db *DB) UserMembers(ctx context.Context, userID xid.ID) (ms []Member, err } // CreateMember creates a member. -func (db *DB) CreateMember(ctx context.Context, tx pgx.Tx, userID xid.ID, name, bio string, links []string) (m Member, err error) { +func (db *DB) CreateMember(ctx context.Context, tx pgx.Tx, userID xid.ID, name string, displayName *string, bio string, links []string) (m Member, err error) { sql, args, err := sq.Insert("members"). - Columns("user_id", "id", "name", "bio", "links"). - Values(userID, xid.New(), name, bio, links). + Columns("user_id", "id", "name", "display_name", "bio", "links"). + Values(userID, xid.New(), name, displayName, bio, links). Suffix("RETURNING *").ToSql() if err != nil { return m, errors.Wrap(err, "building sql") diff --git a/backend/routes/member/create_member.go b/backend/routes/member/create_member.go index ad145ca..840ec12 100644 --- a/backend/routes/member/create_member.go +++ b/backend/routes/member/create_member.go @@ -12,13 +12,14 @@ import ( ) type CreateMemberRequest struct { - Name string `json:"name"` - Bio string `json:"bio"` - Avatar string `json:"avatar"` - Links []string `json:"links"` - Names []db.Name `json:"names"` - Pronouns []db.Pronoun `json:"pronouns"` - Fields []db.Field `json:"fields"` + Name string `json:"name"` + DisplayName *string `json:"display_name"` + Bio string `json:"bio"` + Avatar string `json:"avatar"` + Links []string `json:"links"` + Names []db.Name `json:"names"` + Pronouns []db.Pronoun `json:"pronouns"` + Fields []db.Field `json:"fields"` } func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error) { @@ -54,7 +55,12 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error if cmr.Name == "" { return server.APIError{ Code: server.ErrBadRequest, - Details: "name may not be empty", + 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", } } @@ -76,25 +82,29 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error } defer tx.Rollback(ctx) - m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.Bio, cmr.Links) + m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.DisplayName, cmr.Bio, cmr.Links) if err != nil { + if errors.Cause(err) == db.ErrMemberNameInUse { + return server.APIError{Code: server.ErrMemberNameInUse} + } + return err } // set names, pronouns, fields - err = s.DB.SetMemberNames(ctx, tx, claims.UserID, cmr.Names) + err = s.DB.SetMemberNames(ctx, tx, m.ID, cmr.Names) if err != nil { - log.Errorf("setting names for user %v: %v", claims.UserID, err) + log.Errorf("setting names for member %v: %v", m.ID, err) return err } - err = s.DB.SetMemberPronouns(ctx, tx, claims.UserID, cmr.Pronouns) + err = s.DB.SetMemberPronouns(ctx, tx, m.ID, cmr.Pronouns) if err != nil { - log.Errorf("setting pronouns for user %v: %v", claims.UserID, err) + log.Errorf("setting pronouns for member %v: %v", m.ID, err) return err } - err = s.DB.SetMemberFields(ctx, tx, claims.UserID, cmr.Fields) + err = s.DB.SetMemberFields(ctx, tx, m.ID, cmr.Fields) if err != nil { - log.Errorf("setting fields for user %v: %v", claims.UserID, err) + log.Errorf("setting fields for member %v: %v", m.ID, err) return err } diff --git a/backend/routes/member/get_member.go b/backend/routes/member/get_member.go index 7f3f028..7f9174c 100644 --- a/backend/routes/member/get_member.go +++ b/backend/routes/member/get_member.go @@ -12,11 +12,12 @@ import ( ) type GetMemberResponse struct { - ID xid.ID `json:"id"` - Name string `json:"name"` - Bio *string `json:"bio"` - AvatarURLs []string `json:"avatar_urls"` - Links []string `json:"links"` + ID xid.ID `json:"id"` + Name string `json:"name"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + AvatarURLs []string `json:"avatar_urls"` + Links []string `json:"links"` Names []db.Name `json:"names"` Pronouns []db.Pronoun `json:"pronouns"` @@ -27,11 +28,12 @@ type GetMemberResponse struct { func dbMemberToMember(u db.User, m db.Member, names []db.Name, pronouns []db.Pronoun, fields []db.Field) GetMemberResponse { return GetMemberResponse{ - ID: m.ID, - Name: m.Name, - Bio: m.Bio, - AvatarURLs: m.AvatarURLs, - Links: m.Links, + ID: m.ID, + Name: m.Name, + DisplayName: m.DisplayName, + Bio: m.Bio, + AvatarURLs: m.AvatarURLs, + Links: m.Links, Names: names, Pronouns: pronouns, diff --git a/backend/routes/member/get_members.go b/backend/routes/member/get_members.go index 46a4e79..48814a3 100644 --- a/backend/routes/member/get_members.go +++ b/backend/routes/member/get_members.go @@ -11,11 +11,12 @@ import ( ) type memberListResponse struct { - ID xid.ID `json:"id"` - Name string `json:"name"` - Bio *string `json:"bio"` - AvatarURLs []string `json:"avatar_urls"` - Links []string `json:"links"` + ID xid.ID `json:"id"` + Name string `json:"name"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + AvatarURLs []string `json:"avatar_urls"` + Links []string `json:"links"` } func membersToMemberList(ms []db.Member) []memberListResponse { diff --git a/backend/server/errors.go b/backend/server/errors.go index 0f6c4c9..6162b01 100644 --- a/backend/server/errors.go +++ b/backend/server/errors.go @@ -90,6 +90,7 @@ const ( // Member-related error codes ErrMemberNotFound = 3001 ErrMemberLimitReached = 3002 + ErrMemberNameInUse = 3003 // General request error codes ErrRequestTooBig = 4001 @@ -118,6 +119,7 @@ var errCodeMessages = map[int]string{ ErrMemberNotFound: "Member not found", ErrMemberLimitReached: "Member limit reached", + ErrMemberNameInUse: "Member name already in use", ErrRequestTooBig: "Request too big (max 2 MB)", } @@ -145,6 +147,7 @@ var errCodeStatuses = map[int]int{ ErrMemberNotFound: http.StatusNotFound, ErrMemberLimitReached: http.StatusBadRequest, + ErrMemberNameInUse: http.StatusBadRequest, ErrRequestTooBig: http.StatusBadRequest, } diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 26bb632..4cc8b17 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -19,6 +19,7 @@ export interface User { export interface PartialMember { id: string; name: string; + display_name: string | null; avatar_urls: string[] | null; } diff --git a/scripts/migrate/002_add_member_display_name.sql b/scripts/migrate/002_add_member_display_name.sql new file mode 100644 index 0000000..6bce517 --- /dev/null +++ b/scripts/migrate/002_add_member_display_name.sql @@ -0,0 +1,4 @@ +-- +migrate Up + +-- 2022-11-20: add display name to members +alter table members add column display_name text;