diff --git a/Makefile b/Makefile index f688b79..f1e27cb 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,10 @@ generate: buf generate --path ./proto/user/address.proto buf generate --path ./proto/user/streetaddress.proto buf generate --path ./proto/user/user_messages.proto + buf generate --path ./proto/user/upload_submissions_messages.proto buf generate --path ./proto/user/usergroup_messages.proto + buf generate --path ./proto/user/track_messages.proto + buf generate --path ./proto/user/trackgroup_messages.proto buf generate --path ./proto/user/user.proto # Generate static assets for OpenAPI UI statik -m -f -src third_party/OpenAPI/ diff --git a/authorization/auth_interceptor.go b/authorization/auth_interceptor.go index 353d770..1beffc8 100644 --- a/authorization/auth_interceptor.go +++ b/authorization/auth_interceptor.go @@ -195,43 +195,10 @@ func (interceptor *AuthInterceptor) authorize(ctx context.Context, req interface if isPublicAccessMethod { // everyone can access but check it's against their own ID if activeRole > int32(model.LabelRole) { - // dealing with normal users, Label admins can maintain own artist content. - // attempt to extract the Id from all the possible request types dealing with failure - // TODO a bit nasty, can we make this more elegant and less opinionated? - var id string - userReq, ok := req.(*pbUser.UserRequest) - if !ok { - userUpdateReq, ok := req.(*pbUser.UserUpdateRequest) - if !ok { - userGroupCreateReq, ok := req.(*pbUser.UserGroupCreateRequest) - if !ok { - userGroupUpdateReq, ok := req.(*pbUser.UserGroupUpdateRequest) - if !ok { - return status.Errorf(codes.PermissionDenied, "UUID in request is not valid") - } else { - - newUserGroup := new(model.UserGroup) - - err = interceptor.db.NewSelect(). - Model(newUserGroup). - Where("owner_id = ?", accessTokenRecord.UserID). - Where("id = ?", userGroupUpdateReq.Id). - Scan(ctx) - - if err != nil { - return status.Errorf(codes.PermissionDenied, "Supplied UUID for User Group is not valid or logged in User doesn't own Group") - } - - id = accessTokenRecord.UserID.String() - } - } else { - id = userGroupCreateReq.Id - } - } else { - id = userUpdateReq.Id - } - } else { - id = userReq.Id + id, err := interceptor.extractUserIdFromReq(ctx, req, accessTokenRecord) + + if err != nil { + return err } ID, err := uuid.Parse(id) @@ -314,6 +281,54 @@ func (interceptor *AuthInterceptor) Authenticate(token string) (*model.AccessTok return accessToken, nil } +func (interceptor *AuthInterceptor) extractUserIdFromReq(ctx context.Context, req interface{}, accessTokenRecord *model.AccessToken) (string, error) { + // Dealing with normal users, Label admins can maintain own artist content. + // attempt to extract the Id from all the possible request types dealing with failure + userReq, ok := req.(*pbUser.UserRequest) + + if ok { + return userReq.Id, nil + } + + userUpdateReq, ok := req.(*pbUser.UserUpdateRequest) + + if ok { + return userUpdateReq.Id, nil + } + + userGroupCreateReq, ok := req.(*pbUser.UserGroupCreateRequest) + + if ok { + return userGroupCreateReq.Id, nil + } + + userGroupUpdateReq, ok := req.(*pbUser.UserGroupUpdateRequest) + + if ok { + newUserGroup := new(model.UserGroup) + + err := interceptor.db.NewSelect(). + Model(newUserGroup). + Where("owner_id = ?", accessTokenRecord.UserID). + Where("id = ?", userGroupUpdateReq.Id). + Scan(ctx) + + if err != nil { + return "", status.Errorf(codes.PermissionDenied, "Supplied UUID for User Group is not valid or logged in User doesn't own Group") + } + + return accessTokenRecord.UserID.String(), nil + } + + uploadSubmissionAddReq, ok := req.(*pbUser.UploadSubmissionAddRequest) + + if ok { + return uploadSubmissionAddReq.Id, nil + } + + return "", status.Errorf(codes.PermissionDenied, "UUID in request is not valid") +} + func (interceptor *AuthInterceptor) find(slice []string, val string) (int, bool) { for i, item := range slice { if item == val { diff --git a/conf.local.yaml b/conf.local.yaml index cc223d3..0a97b93 100644 --- a/conf.local.yaml +++ b/conf.local.yaml @@ -18,8 +18,8 @@ refreshtoken: access: no_token_methods: "/user.ResonateUser/AddUser,/user.ResonateUser/GetUserGroup" - public_methods: "/user.ResonateUser/GetUser,/user.ResonateUser/GetUserCredits,/user.ResonateUser/GetUserMembership,/user.ResonateUser/UpdateUser,/user.ResonateUser/AddUserGroup,/user.ResonateUser/UpdateUserGroup,/user.ResonateUser/ListUsersUserGroups" - write_methods: "/user.ResonateUser/DeleteUser,/user.ResonateUser/UpdateUser,/user.ResonateUser/AddUserGroup,/user.ResonateUser/UpdateUserGroup" + public_methods: "/user.ResonateUser/AddUploadSubmission,/user.ResonateUser/UpdateUploadSubmission,/user.ResonateUser/DeleteUploadSubmission,/user.ResonateUser/GetUser,/user.ResonateUser/GetUserCredits,/user.ResonateUser/GetUserMembership,/user.ResonateUser/UpdateUser,/user.ResonateUser/AddUserGroup,/user.ResonateUser/UpdateUserGroup,/user.ResonateUser/ListUsersUserGroups" + write_methods: "/user.ResonateUser/AddUploadSubmission,/user.ResonateUser/UpdateUploadSubmission,/user.ResonateUser/DeleteUploadSubmission,/user.ResonateUser/DeleteUser,/user.ResonateUser/UpdateUser,/user.ResonateUser/AddUserGroup,/user.ResonateUser/UpdateUserGroup" application: min_password_strength: 0 # Minimum password zxcvbn strength diff --git a/migrations/22052021010104_trackserver.go b/migrations/22052021010104_trackserver.go new file mode 100644 index 0000000..35d303d --- /dev/null +++ b/migrations/22052021010104_trackserver.go @@ -0,0 +1,60 @@ +package migrations + +import ( + "context" + "fmt" + + "github.com/uptrace/bun" + + "github.com/resonatecoop/user-api/model" +) + +func init() { + + // Drop and create tables. + models := []interface{}{ + (*model.UploadSubmission)(nil), + (*model.Track)(nil), + (*model.TrackGroup)(nil), + (*model.Play)(nil), + } + + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [up migration] ") + + if _, err := db.Exec(`CREATE TYPE track_status AS ENUM ('paid', 'free', 'both');`); err != nil { + return err + } + + if _, err := db.Exec(`CREATE TYPE play_type AS ENUM ('paid', 'free');`); err != nil { + return err + } + + if _, err := db.Exec(`CREATE TYPE track_group_type AS ENUM ('lp', 'ep', 'single', 'playlist');`); err != nil { + return err + } + + for _, model := range models { + _, err := db.NewDropTable().Model(model).IfExists().Exec(ctx) + if err != nil { + panic(err) + } + + _, err = db.NewCreateTable().Model(model).Exec(ctx) + if err != nil { + panic(err) + } + } + + return nil + }, func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [down migration] ") + for _, this_model := range models { + _, err := db.NewDropTable().Model(this_model).IfExists().Exec(ctx) + if err != nil { + panic(err) + } + } + return nil + }) +} diff --git a/model/play.go b/model/play.go new file mode 100644 index 0000000..cce377c --- /dev/null +++ b/model/play.go @@ -0,0 +1,31 @@ +package model + +import ( + "context" + + uuid "github.com/google/uuid" + "github.com/uptrace/bun" +) + +type Play struct { + IDRecord + UserId uuid.UUID `bun:",type:uuid,notnull"` + TrackId uuid.UUID `bun:",type:uuid,notnull"` + Type string `bun:"type:play_type,notnull"` + Credits float32 `bun:",notnull"` +} + +// Count number of times a track has been played (and paid) by a user +func CountPlays(ctx context.Context, db *bun.DB, trackId uuid.UUID, userId uuid.UUID) (int32, error) { + play := Play{} + count, err := db.NewSelect(). + Model(play). + Where("user_id = ?", userId). + Where("track_id = ?", trackId). + Where("type = 'paid'"). + Count(ctx) + if err != nil { + return 0, err + } + return int32(count), nil +} diff --git a/model/track.go b/model/track.go new file mode 100644 index 0000000..c19015b --- /dev/null +++ b/model/track.go @@ -0,0 +1,36 @@ +package model + +import ( + uuid "github.com/google/uuid" +) + +type Track struct { + IDRecord + Title string `bun:",notnull"` + Status string `bun:"type:track_status,notnull"` + Enabled bool `bun:",notnull"` + TrackNumber int32 `bun:",notnull"` + Duration float32 + Download bool `bun:",notnull"` // Allow or disallow download + + TrackGroups []uuid.UUID `bun:",type:uuid[],array"` + FavoriteOfUsers []uuid.UUID `bun:",type:uuid[],array"` + + TrackServerId uuid.UUID `bun:"type:uuid,notnull"` + + OwnerId uuid.UUID `bun:"type:uuid,notnull"` + Owner *User `bun:"rel:has-one"` + + UserGroupId uuid.UUID `bun:"type:uuid,notnull"` + UserGroup *UserGroup `bun:"rel:has-one"` // track belongs to user group (the one who gets paid) + + Artists []uuid.UUID `bun:",type:uuid[]" pg:",array"` // for display purposes + Tags []uuid.UUID `bun:",type:uuid[]" pg:",array"` + + Composers map[string]string `pg:",hstore"` + Performers map[string]string `pg:",hstore"` + + ISRC string + + // Plays []User `pg:"many2many:plays"` Payment API +} diff --git a/model/track_group.go b/model/track_group.go new file mode 100644 index 0000000..1eab1e9 --- /dev/null +++ b/model/track_group.go @@ -0,0 +1,40 @@ +package model + +import ( + "time" + + "github.com/google/uuid" +) + +// TrackGroup +type TrackGroup struct { + IDRecord + + Title string `bun:",notnull"` + Slug string `bun:",notnull"` // Slug title + ReleaseDate time.Time `bun:",notnull"` + Type string `bun:"type:track_group_type,notnull"` // EP, LP, Single, Playlist + Cover []byte `bun:",notnull"` + DisplayArtist string // for display purposes, e.g. "Various" for compilation + MultipleComposers bool `bun:",notnull"` + Private bool `bun:",notnull"` + About string + + OwnerId uuid.UUID `bun:"type:uuid,notnull"` + Owner *User `bun:"rel:has-one"` + + UserGroupId uuid.UUID `bun:"type:uuid,default:uuid_nil()"` // track group belongs to user group, can be null if user playlist + LabelId uuid.UUID `bun:"type:uuid,default:uuid_nil()"` + + Tracks []uuid.UUID `bun:",type:uuid[]" pg:",array"` + Tags []uuid.UUID `bun:",type:uuid[]" pg:",array"` + + TerritoriesIncl []string `pg:",array"` + CLineYear time.Time + PLineYear time.Time + CLineText string + PLineText string + RightExpiryDate time.Time + TotalVolumes int + CatalogNumber string +} diff --git a/model/upload_submission.go b/model/upload_submission.go new file mode 100644 index 0000000..9c276a0 --- /dev/null +++ b/model/upload_submission.go @@ -0,0 +1,16 @@ +package model + +import "github.com/google/uuid" + +// UploadSubmission +type UploadSubmission struct { + IDRecord + Active bool `bun:"default:true,notnull"` + Description string `bun:",notnull"` + Files []uuid.UUID `bun:",type:uuid[],array"` + Name string `bun:",notnull"` + TrackGroup *TrackGroup `bun:"rel:has-one"` + TrackGroupID uuid.UUID `bun:"type:uuid"` + UserID uuid.UUID `bun:"type:uuid,notnull"` + User *User `bun:"rel:has-one"` +} diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go index d42b10a..0a14aef 100644 --- a/pkg/uuid/uuid.go +++ b/pkg/uuid/uuid.go @@ -1,6 +1,8 @@ package uuidpkg import ( + "errors" + uuid "github.com/google/uuid" ) @@ -9,7 +11,7 @@ func IsValidUUID(u string) bool { return err == nil } -//ConvertUUIDToStrArray returns a slice of uuids for given slive of strings +//ConvertUUIDToStrArray returns a slice of strings for given slice of uuids func ConvertUUIDToStrArray(uuids []uuid.UUID) []string { strArray := make([]string, len(uuids)) for i := range uuids { @@ -18,14 +20,26 @@ func ConvertUUIDToStrArray(uuids []uuid.UUID) []string { return strArray } -// // GetUUIDFromString returns id as string and returns error if not a valid uuid -// func GetUUIDFromString(id string) (uuid.UUID, twirp.Error) { -// uid, err := uuid.FromString(id) -// if err != nil { -// return uuid.UUID{}, twirp.InvalidArgumentError("id", "must be a valid uuid") -// } -// return uid, nil -// } +//ConvertUUIDToStrArray returns a slice of uuids for given slice of strings +func ConvertStrToUUIDArray(str []string) []uuid.UUID { + uuidArray := make([]uuid.UUID, len(str)) + for i := range str { + u, err := uuid.Parse(str[i]) + if err != nil { + uuidArray[i] = u + } + } + return uuidArray +} + +// GetUUIDFromString returns id as string and returns error if not a valid uuid +func GetUUIDFromString(id string) (uuid.UUID, error) { + uid, err := uuid.Parse(id) + if err != nil { + return uuid.UUID{}, errors.New("must be a valid uuid") + } + return uid, nil +} // Difference returns difference between two slices of uuids func Difference(a, b []uuid.UUID) []uuid.UUID { diff --git a/proto/user/common.proto b/proto/user/common.proto index c565270..c36c9b4 100644 --- a/proto/user/common.proto +++ b/proto/user/common.proto @@ -14,6 +14,18 @@ message RelatedUserGroup { bytes avatar = 3; } +message RelatedTrackGroup { + string id = 1; // required + string title = 2; // required + bytes cover = 3; // required + string type = 4; // required + string about = 5; + bool private = 6; + string display_artist = 7; + int32 total_tracks = 8; + RelatedUserGroup user_group = 9; +} + message User { string id = 1; // required string username = 2; // required diff --git a/proto/user/track_messages.proto b/proto/user/track_messages.proto new file mode 100644 index 0000000..2dc6d3b --- /dev/null +++ b/proto/user/track_messages.proto @@ -0,0 +1,30 @@ +syntax="proto3"; + +import "user/common.proto"; + +//package example; +//package resonate.api.user; +package user; + +option go_package = "github.com/resonatecoop/user-api/proto/user"; + +message Track { + string id = 1; // required + + string title = 2; // required + string status = 3; // required + bool enabled = 4; + int32 track_number = 5; + + repeated RelatedTrackGroup track_groups = 6; + string creator_id = 7; + string user_group_id = 8; + repeated RelatedUserGroup artists = 9; + string track_server_id = 10; + repeated Tag tags = 11; + float duration = 12; +} + +message TracksList { + repeated Track tracks = 1; +} diff --git a/proto/user/trackgroup_messages.proto b/proto/user/trackgroup_messages.proto new file mode 100644 index 0000000..cb57eff --- /dev/null +++ b/proto/user/trackgroup_messages.proto @@ -0,0 +1,63 @@ +syntax="proto3"; + +import "google/protobuf/timestamp.proto"; +import "user/common.proto"; +import "user/track_messages.proto"; +import "user/usergroup_messages.proto"; + +//package example; +//package resonate.api.user; +package user; +option go_package = "github.com/resonatecoop/user-api/proto/user"; + +message TrackGroupRequest { + string id = 1; +} + +message TrackGroupCreateRequest { + string id = 1; // required + string title = 2; // required + bytes cover = 3; // required + google.protobuf.Timestamp release_date = 4; + string type = 5; // required + string display_artist = 6; + bool multiple_composers = 7; + bool private = 8; + string creator_id = 9; + string user_group_id = 10; + RelatedUserGroup UserGroup = 11; + string label_id = 12; + RelatedUserGroup Label = 13; + repeated Track tracks = 14; + repeated Tag tags = 15; + string about = 16; +} + +message TrackGroupUpdateRequest { + string id = 1; // required + string title = 2; // required + bytes cover = 3; // required + google.protobuf.Timestamp release_date = 4; + string type = 5; // required + string display_artist = 6; + bool multiple_composers = 7; + bool private = 8; + string creator_id = 9; + string user_group_id = 10; + RelatedUserGroup UserGroup = 11; + string label_id = 12; + RelatedUserGroup Label = 13; + repeated Track tracks = 14; + repeated Tag tags = 15; + string about = 16; +} + +message TrackGroupResponse { + string id = 1; + string title = 2; +} + +message TracksToTrackGroup { + string track_group_id = 1; // required + repeated Track tracks = 2; // required +} diff --git a/proto/user/upload_submissions_messages.proto b/proto/user/upload_submissions_messages.proto new file mode 100644 index 0000000..de2b2b3 --- /dev/null +++ b/proto/user/upload_submissions_messages.proto @@ -0,0 +1,24 @@ +syntax="proto3"; + +//package example; +//package resonate.api.user; +package user; +option go_package = "github.com/resonatecoop/user-api/proto/user"; + +message UploadSubmissionRequest { + string id = 1; +} + +message UploadSubmissionAddRequest { + string id = 1; // UUID required + string name = 2; // required + string description = 3; // required + repeated string files = 4; +} + +message UploadSubmissionUpdateRequest { + string id = 1; // UUID required + string name = 2; // required + string description = 3; // required + repeated string files = 4; +} diff --git a/proto/user/user.proto b/proto/user/user.proto index ae347c9..df30853 100644 --- a/proto/user/user.proto +++ b/proto/user/user.proto @@ -10,6 +10,9 @@ import "google/api/annotations.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "user/common.proto"; import "user/user_messages.proto"; +import "user/track_messages.proto"; +import "user/trackgroup_messages.proto"; +import "user/upload_submissions_messages.proto"; import "user/usergroup_messages.proto"; // Defines the import path that should be used to import the generated package, @@ -19,13 +22,23 @@ option go_package = "github.com/resonatecoop/user-api/proto/user"; // These annotations are used when generating the OpenAPI file. option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - version: "2.0.2"; - title: "Resonate Service Documentation: User"; + version: "2.0.2" + title: "Resonate Service Documentation: User" + // description: 'User API' + license: { + name: "MIT License" + url: "https://github.com/resonatecoop/user-api/blob/master/LICENSE" + } + contact: { + email: "members@resonate.coop" + } }; + // host: "https://api.resonate.coop"; + // base_path: "/api/v1"; external_docs: { - url: "https://github.com/resonatecoop/user-api"; - description: "gRPC-gateway resonate-user-api repository"; - } + url: "https://github.com/resonatecoop/user-api" + description: "gRPC-gateway resonate-user-api repository" + }; security_definitions: { security: { key: "bearer" @@ -36,17 +49,16 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { description: "Authentication token, prefixed by Bearer: Bearer " } } - } + }; security: { security_requirement: { key: "bearer" } - } + }; schemes: HTTPS; }; service ResonateUser { - // Users //GetUser provides a public level of information about a user @@ -178,6 +190,48 @@ service ResonateUser { }; } + // UploadSubmissions + + //AddUploadSubmission adds a UserUploadSubmission + rpc AddUploadSubmission(UploadSubmissionAddRequest) returns (UploadSubmissionRequest) { + option (google.api.http) = { + // Route to this method from POST requests to /api/v1/users/{id}/upload_submission + post: "/api/v1/users/{id}/upload-submission" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Add an upload submission" + description: "Add an upload submission to the server to user resource with id: id." + tags: "UploadSubmissions" + }; + } + + //UpdateUploadSubmission updates an existing UploadSubmission + rpc UpdateUploadSubmission(UploadSubmissionUpdateRequest) returns (Empty) { + option (google.api.http) = { + patch: "/api/v1/upload-submission/{id}" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Update an upload submission" + description: "Update an existing upload submission record on the server." + tags: "UploadSubmissions" + }; + } + + //DeleteUploadSubmission deletes an upload submission + rpc DeleteUploadSubmission(UploadSubmissionRequest) returns (Empty) { + option (google.api.http) = { + // Route to this method from DELETE requests to /api/v1/restricted/user/{id} + delete: "/api/v1/upload-submission/{id}" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Delete upload submission" + description: "Delete an upload submission from the server." + tags: "UploadSubmissions" + }; + } + // UserGroups //AddUserGroup adds a UserGroup based on provided attributes @@ -287,11 +341,7 @@ service ResonateUser { }; } - // rpc CreateUserGroup(UserGroupCreateRequest) returns (UserGroupPrivateResponse); - // rpc GetUserGroup(UserGroupRequest) returns (UserGroupPublicResponse); - // // rpc GetUserGroupRestricted(UserGroupRequest) returns (UserGroupPrivateResponse); - // rpc UpdateUserGroup(UserGroupUpdateRequest) returns (UserGroupPrivateResponse); - // rpc DeleteUserGroup(UserGroupRequest) returns (Empty); + // rpc GetUserGroupRestricted(UserGroupRequest) returns (UserGroupPrivateResponse); // rpc GetChildUserGroups(UserGroupRequest) returns (GroupedUserGroups); // rpc GetParentUserGroups(UserGroupRequest) returns (GroupedUserGroups); @@ -299,13 +349,69 @@ service ResonateUser { // rpc GetLabelUserGroups(UserGroupRequest) returns (GroupedUserGroups); // rpc GetUserGroupTypes(Empty) returns (GroupTaxonomies); - // //rpc AddRecommended(UserGroupRecommended) returns (Empty); - // //rpc RemoveRecommended(UserGroupRecommended) returns (Empty); + // rpc AddRecommended(UserGroupRecommended) returns (Empty); + // rpc RemoveRecommended(UserGroupRecommended) returns (Empty); // rpc AddMember(UserGroupMembershipRequest) returns (Empty); // rpc DeleteMember(UserGroupMembershipRequest) returns (Empty); // rpc SearchUserGroups(Query) returns (SearchResults); + + // User Library + // rpc GetPlaylists(User) returns (Playlists); + // rpc GetFavoriteTracks(User) returns (Tracks); + // rpc GetOwnedTracks(User) returns (Tracks); + // rpc GetTrackHistory(User) returns (Tracks); + + // rpc GetSupportedArtists(User) returns (Artists); + // rpc CreatePlay(CreatePlayRequest) returns (CreatePlayResponse); Payment API + + // rpc FollowGroup(UserToUserGroup) returns (Empty); + // rpc UnfollowGroup(UserToUserGroup) returns (Empty); + // rpc AddFavoriteTrack(UserToTrack) returns (Empty); + // rpc RemoveFavoriteTrack(UserToTrack) returns (Empty); + + // Tracks + + // rpc CreateTrack(Track) returns (Track); + // rpc GetTracks(TracksList) returns (TracksList); + // rpc UpdateTrack(Track) returns (Empty); + // rpc DeleteTrack(Track) returns (Empty); + + // rpc SearchTracks(Query) returns (SearchResults); + + // Trackgroups + + rpc CreateTrackGroup(TrackGroupCreateRequest) returns (TrackGroupResponse) { + option (google.api.http) = { + // Route to this method from POST requests to /api/v1/trackgroup + post: "/api/v1/trackgroup" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Create a trackgroup" + description: "Create a trackgroup to the server." + tags: "Trackgroups" + }; + } + + rpc GetTrackGroup(TrackGroupRequest) returns (TrackGroupResponse) { + option (google.api.http) = { + // Route to this method from GET requests to /api/v1/trackgroup/{id} + get: "/api/v1/trackgroup/{id}" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get a trackgroup" + description: "Get a trackgroup from the server." + tags: "Trackgroups" + }; + } + // rpc UpdateTrackGroup(TrackGroup) returns (Empty); + // rpc DeleteTrackGroup(TrackGroup) returns (Empty); + // rpc AddTracksToTrackGroup(TracksToTrackGroup) returns (Empty); + // rpc RemoveTracksFromTrackGroup(TracksToTrackGroup) returns (Empty); + + // rpc SearchTrackGroups(Query) returns (SearchResults); } diff --git a/proto/user/user_messages.proto b/proto/user/user_messages.proto index 07ce375..e0d3a59 100644 --- a/proto/user/user_messages.proto +++ b/proto/user/user_messages.proto @@ -171,8 +171,6 @@ message UserListResponse { // repeated Track tracks = 1; // } - - // message Artists { // repeated RelatedUserGroup artists = 1; // } diff --git a/server/track.go b/server/track.go new file mode 100644 index 0000000..50c2715 --- /dev/null +++ b/server/track.go @@ -0,0 +1,154 @@ +package server + +import ( + "context" + "fmt" + + "github.com/resonatecoop/user-api/model" + + pbUser "github.com/resonatecoop/user-api/proto/user" +) + +// func (s *Server) GetTracks(ctx context.Context, req *pbUser.TracksList) (*pbUser.TracksList, error) { +// trackIds := make([]uuid.UUID, len(req.Tracks)) +// for i, track := range req.Tracks { +// id, err := uuidpkg.GetUuidFromString(track.Id) +// if err != nil { +// return nil, err +// } +// trackIds[i] = id +// } +// tracksResponse, twerr := model.GetTracks(trackIds, s.db, true, ctx) +// if twerr != nil { +// return nil, twerr +// } +// return &pb.TracksList{ +// Tracks: tracksResponse, +// }, nil +// } +// +// func (s *Server) SearchTracks(ctx context.Context, q *pbUser.Query) (*pbUser.SearchResults, error) { +// if len(q.Query) < 3 { +// return nil, errors.New("query must be a valid search query") +// } +// +// searchResults, twerr := model.SearchTracks(q.Query, s.db) +// if twerr != nil { +// return nil, twerr +// } +// return searchResults, nil +// } + +// CreateTrack +func (s *Server) CreateTrack(ctx context.Context, track *pbUser.Track) (*pbUser.Track, error) { + // Track is created then added to a TrackGroup on track group creation + err := checkRequiredAttributes(track) + if err != nil { + return nil, err + } + + t := &model.Track{ + Title: track.Title, + Status: track.Status, + Enabled: track.Enabled, + TrackNumber: track.TrackNumber, + Duration: track.Duration, + } + + _, err = s.db.NewInsert(). + Column( + "id", + "title", + "status", + "duration", + "track_number", + ). + Model(t). + Exec(ctx) + + res := &pbUser.Track{Id: t.ID.String()} + + return res, nil +} + +// GetTrack +func (s *Server) GetTrack(ctx context.Context, track *pbUser.Track) (*pbUser.Track, error) { + // t, err := getTrackModel(track) + // if err != nil { + // return nil, err + // } + // + // pgerr := s.db.Model(t). + // Column("track.*"). + // WherePK(). + // Select() + // if pgerr != nil { + // return nil, errorpkg.CheckError(pgerr, "track") + // } + // track.UserGroupId = t.UserGroupId.String() + // track.CreatorId = t.CreatorId.String() + // track.TrackServerId = t.TrackServerId.String() + // track.Title = t.Title + // track.Status = t.Status + // track.Enabled = t.Enabled + // track.TrackNumber = t.TrackNumber + // track.Duration = t.Duration + // + // // Get tags + // tags, twerr := model.GetTags(t.Tags, s.db) + // if twerr != nil { + // return nil, twerr + // } + // track.Tags = tags + // + // // Get artists (id, name, avatar) + // artists, pgerr := model.GetRelatedUserGroups(t.Artists, s.db) + // if pgerr != nil { + // return nil, errorpkg.CheckError(pgerr, "user_group") + // } + // track.Artists = artists + // + // // Get track_groups (id, title, cover) that are not playlists (i.e. LP, EP or Single) + // trackGroups, twerr := model.GetTrackGroupsFromIds(t.TrackGroups, s.db, []string{"lp", "ep", "single"}) + // if twerr != nil { + // return nil, twerr + // } + // track.TrackGroups = trackGroups + + return &pbUser.Track{}, nil +} + +// UpdateTrack +func (s *Server) UpdateTrack(ctx context.Context, track *pbUser.Track) (*pbUser.Empty, error) { + err := checkRequiredAttributes(track) + if err != nil { + return nil, err + } + + return &pbUser.Empty{}, nil +} + +// DeleteTrack +func (s *Server) DeleteTrack(ctx context.Context, track *pbUser.Track) (*pbUser.Empty, error) { + return &pbUser.Empty{}, nil +} + +func checkRequiredAttributes(track *pbUser.Track) error { + if track.Title == "" || track.Status == "" || track.TrackNumber == 0 || track.CreatorId == "" || track.UserGroupId == "" { // track.Artists? + var argument string + switch { + case track.Title == "": + argument = "title" + case track.Status == "": + argument = "status" + case track.CreatorId == "": + argument = "creator_id" + case track.UserGroupId == "": + argument = "user_group_id" + case track.TrackNumber == 0: + argument = "track_number" + } + return fmt.Errorf("argument %v is required", argument) + } + return nil +} diff --git a/server/trackgroup.go b/server/trackgroup.go new file mode 100644 index 0000000..77c770c --- /dev/null +++ b/server/trackgroup.go @@ -0,0 +1,65 @@ +package server + +import ( + "context" + "errors" + "fmt" + + pbUser "github.com/resonatecoop/user-api/proto/user" +) + +// GetTrackGroup +func (s *Server) GetTrackGroup(ctx context.Context, trackgroup *pbUser.TrackGroupRequest) (*pbUser.TrackGroupResponse, error) { + return &pbUser.TrackGroupResponse{}, nil +} + +// CreateTrackGroup +func (s *Server) CreateTrackGroup(ctx context.Context, trackgroup *pbUser.TrackGroupCreateRequest) (*pbUser.TrackGroupResponse, error) { + return &pbUser.TrackGroupResponse{}, nil +} + +// UpdateTrackGroup +func (s *Server) UpdateTrackGroup(ctx context.Context, trackgroup *pbUser.TrackGroupUpdateRequest) (*pbUser.Empty, error) { + return &pbUser.Empty{}, nil +} + +// DeleteTrackGroup +func (s *Server) DeleteTrackGroup(ctx context.Context, trackGroup *pbUser.TrackGroupRequest) (*pbUser.Empty, error) { + return &pbUser.Empty{}, nil +} + +// AddTracksToTrackGroup +func (s *Server) AddTracksToTrackGroup(ctx context.Context, tracksToTrackGroup *pbUser.TracksToTrackGroup) (*pbUser.Empty, error) { + return &pbUser.Empty{}, nil +} + +// RemoveTracksFromTrackGroup +func (s *Server) RemoveTracksFromTrackGroup(ctx context.Context, tracksToTrackGroup *pbUser.TracksToTrackGroup) (*pbUser.Empty, error) { + return &pbUser.Empty{}, nil +} + +func checkTrackGroupRequiredAttributes(trackGroup *pbUser.TrackGroupCreateRequest) error { + if trackGroup.Title == "" || (trackGroup.ReleaseDate == nil) || trackGroup.Type == "" || len(trackGroup.Cover) == 0 || trackGroup.CreatorId == "" { + var argument string + switch { + case trackGroup.Title == "": + argument = "title" + case trackGroup.ReleaseDate == nil: + argument = "release_date" + case trackGroup.Type == "": + argument = "type" + case len(trackGroup.Cover) == 0: + argument = "cover" + case trackGroup.CreatorId == "": + argument = "creator_id" + } + return fmt.Errorf("argument %v is required", argument) + } + // A playlist does not have necessarily a owner user group (with id UserGroupId) + // if it is a private user playlist + // But other types of track groups (lp, ep, single) have to belong to a user group + if trackGroup.Type != "playlist" && trackGroup.UserGroupId == "" { + return errors.New("user_group_id is required") + } + return nil +} diff --git a/server/upload_submission.go b/server/upload_submission.go new file mode 100644 index 0000000..0e3f9a9 --- /dev/null +++ b/server/upload_submission.go @@ -0,0 +1,123 @@ +package server + +import ( + "context" + "errors" + "time" + + uuid "github.com/google/uuid" + "github.com/resonatecoop/user-api/model" + uuidpkg "github.com/resonatecoop/user-api/pkg/uuid" + pbUser "github.com/resonatecoop/user-api/proto/user" +) + +// AddUserUploadSubmission +func (s *Server) AddUploadSubmission( + ctx context.Context, + uploadSubmission *pbUser.UploadSubmissionAddRequest, +) ( + *pbUser.UploadSubmissionRequest, + error, +) { + UserUUID, err := uuid.Parse(uploadSubmission.Id) + + if err != nil { + return nil, errors.New("supplied user_id is not a valid UUID") + } + + newUploadSubmission := &model.UploadSubmission{ + Name: uploadSubmission.Name, + Description: uploadSubmission.Description, + Files: uuidpkg.ConvertStrToUUIDArray(uploadSubmission.Files), + UserID: UserUUID, + } + + newUploadSubmission.ID = uuid.Must(uuid.NewRandom()) + newUploadSubmission.CreatedAt = time.Now().UTC() + + _, err = s.db.NewInsert(). + Column( + "id", + "user_id", + "name", + "description", + "files", + ). + Model(newUploadSubmission). + Exec(ctx) + + if err != nil { + return nil, err + } + + res := &pbUser.UploadSubmissionRequest{ + Id: newUploadSubmission.ID.String(), + } + + return res, nil +} + +// UpdateUserUploadSubmission +func (s *Server) UpdateUploadSubmission( + ctx context.Context, + UploadSubmissionUpdateRequest *pbUser.UploadSubmissionUpdateRequest, +) ( + *pbUser.Empty, + error, +) { + uploadSubmission := new(model.UploadSubmission) + + err := s.db.NewSelect(). + Model(uploadSubmission). + Where("id = ?", UploadSubmissionUpdateRequest.Id). + Limit(1). + Scan(ctx) + + // Not found + if err != nil { + return nil, errors.New("upload submission not found") + } + + if UploadSubmissionUpdateRequest.Files != nil { + uploadSubmission.Files = uuidpkg.ConvertStrToUUIDArray(UploadSubmissionUpdateRequest.Files) + } + + rows, err := s.db.NewUpdate(). + Column( + "id", + "name", + "description", + "files", + ). + Model(uploadSubmission). + WherePK(). + Exec(ctx) + + if err != nil { + return nil, err + } + + number, _ := rows.RowsAffected() + + if number == 0 { + return nil, errors.New("warning: no rows were updated") + } + + return &pbUser.Empty{}, nil +} + +// DeleteUploadSubmission Deletes an upload submission +func (s *Server) DeleteUploadSubmission(ctx context.Context, uploadSubmission *pbUser.UploadSubmissionRequest) (*pbUser.Empty, error) { + u := new(model.UploadSubmission) + + _, err := s.db.NewDelete(). + Model(u). + Where("id = ?", uploadSubmission.Id). + Exec(ctx) + + if err != nil { + return nil, err + } + + return &pbUser.Empty{}, nil +} diff --git a/third_party/OpenAPI/user/track_messages.swagger.json b/third_party/OpenAPI/user/track_messages.swagger.json new file mode 100644 index 0000000..693a126 --- /dev/null +++ b/third_party/OpenAPI/user/track_messages.swagger.json @@ -0,0 +1,46 @@ +{ + "swagger": "2.0", + "info": { + "title": "package example;\npackage resonate.api.user;", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "typeUrl": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/third_party/OpenAPI/user/trackgroup_messages.swagger.json b/third_party/OpenAPI/user/trackgroup_messages.swagger.json new file mode 100644 index 0000000..693a126 --- /dev/null +++ b/third_party/OpenAPI/user/trackgroup_messages.swagger.json @@ -0,0 +1,46 @@ +{ + "swagger": "2.0", + "info": { + "title": "package example;\npackage resonate.api.user;", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "typeUrl": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/third_party/OpenAPI/user/upload_submissions_messages.swagger.json b/third_party/OpenAPI/user/upload_submissions_messages.swagger.json new file mode 100644 index 0000000..693a126 --- /dev/null +++ b/third_party/OpenAPI/user/upload_submissions_messages.swagger.json @@ -0,0 +1,46 @@ +{ + "swagger": "2.0", + "info": { + "title": "package example;\npackage resonate.api.user;", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "typeUrl": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/third_party/OpenAPI/user/user.swagger.json b/third_party/OpenAPI/user/user.swagger.json index e256447..24c3749 100644 --- a/third_party/OpenAPI/user/user.swagger.json +++ b/third_party/OpenAPI/user/user.swagger.json @@ -2,7 +2,14 @@ "swagger": "2.0", "info": { "title": "Resonate Service Documentation: User", - "version": "2.0.2" + "version": "2.0.2", + "contact": { + "email": "members@resonate.coop" + }, + "license": { + "name": "MIT License", + "url": "https://github.com/resonatecoop/user-api/blob/master/LICENSE" + } }, "schemes": [ "https" @@ -114,6 +121,142 @@ ] } }, + "/api/v1/trackgroup": { + "post": { + "summary": "Create a trackgroup", + "description": "Create a trackgroup to the server.", + "operationId": "ResonateUser_CreateTrackGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/userTrackGroupResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/userTrackGroupCreateRequest" + } + } + ], + "tags": [ + "Trackgroups" + ] + } + }, + "/api/v1/trackgroup/{id}": { + "get": { + "summary": "Get a trackgroup", + "description": "Get a trackgroup from the server.", + "operationId": "ResonateUser_GetTrackGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/userTrackGroupResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "Trackgroups" + ] + } + }, + "/api/v1/upload-submission/{id}": { + "delete": { + "summary": "Delete upload submission", + "description": "Delete an upload submission from the server.", + "operationId": "ResonateUser_DeleteUploadSubmission", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/userEmpty" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "UploadSubmissions" + ] + }, + "patch": { + "summary": "Update an upload submission", + "description": "Update an existing upload submission record on the server.", + "operationId": "ResonateUser_UpdateUploadSubmission", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/userEmpty" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/userUploadSubmissionUpdateRequest" + } + } + ], + "tags": [ + "UploadSubmissions" + ] + } + }, "/api/v1/user/{id}": { "get": { "summary": "Get a user", @@ -404,6 +547,46 @@ ] } }, + "/api/v1/users/{id}/upload-submission": { + "post": { + "summary": "Add an upload submission", + "description": "Add an upload submission to the server to user resource with id: id.", + "operationId": "ResonateUser_AddUploadSubmission", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/userUploadSubmissionRequest" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/userUploadSubmissionAddRequest" + } + } + ], + "tags": [ + "UploadSubmissions" + ] + } + }, "/api/v1/users/{id}/usergroup": { "post": { "summary": "Add a user group", @@ -531,6 +714,241 @@ } } }, + "userRelatedTrackGroup": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "cover": { + "type": "string", + "format": "byte" + }, + "type": { + "type": "string" + }, + "about": { + "type": "string" + }, + "private": { + "type": "boolean" + }, + "displayArtist": { + "type": "string" + }, + "totalTracks": { + "type": "integer", + "format": "int32" + }, + "userGroup": { + "$ref": "#/definitions/userRelatedUserGroup" + } + } + }, + "userRelatedUserGroup": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "avatar": { + "type": "string", + "format": "byte" + } + } + }, + "userTag": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "userTrack": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "trackNumber": { + "type": "integer", + "format": "int32" + }, + "trackGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/userRelatedTrackGroup" + } + }, + "creatorId": { + "type": "string" + }, + "userGroupId": { + "type": "string" + }, + "artists": { + "type": "array", + "items": { + "$ref": "#/definitions/userRelatedUserGroup" + } + }, + "trackServerId": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/userTag" + } + }, + "duration": { + "type": "number", + "format": "float" + } + } + }, + "userTrackGroupCreateRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "cover": { + "type": "string", + "format": "byte" + }, + "releaseDate": { + "type": "string", + "format": "date-time" + }, + "type": { + "type": "string" + }, + "displayArtist": { + "type": "string" + }, + "multipleComposers": { + "type": "boolean" + }, + "private": { + "type": "boolean" + }, + "creatorId": { + "type": "string" + }, + "userGroupId": { + "type": "string" + }, + "UserGroup": { + "$ref": "#/definitions/userRelatedUserGroup" + }, + "labelId": { + "type": "string" + }, + "Label": { + "$ref": "#/definitions/userRelatedUserGroup" + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/userTrack" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/userTag" + } + }, + "about": { + "type": "string" + } + } + }, + "userTrackGroupResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "userUploadSubmissionAddRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "userUploadSubmissionRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "userUploadSubmissionUpdateRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "userUserAddRequest": { "type": "object", "properties": {