package subsonic import ( "encoding/hex" "strconv" "strings" vsauth "git.noroutes.net/NoRoutes/vibetunes/internal/auth" "git.noroutes.net/NoRoutes/vibetunes/internal/database" ) func init() { register("getUser", handleGetUser) register("getUsers", handleGetUsers) register("createUser", handleCreateUser) register("updateUser", handleUpdateUser) register("deleteUser", handleDeleteUser) register("changePassword", handleChangePassword) } func userToModel(rc *Context, u *database.User) User { db := rc.Server.DB m := User{ Username: u.Username, Email: u.Email, ScrobblingEnabled: true, AdminRole: u.IsAdmin, SettingsRole: u.Allows(u.Permissions.Settings), DownloadRole: u.Allows(u.Permissions.Download), UploadRole: u.Allows(u.Permissions.Upload), PlaylistRole: u.Allows(u.Permissions.Playlist), CoverArtRole: u.Allows(u.Permissions.CoverArt), CommentRole: u.Allows(u.Permissions.Comment), PodcastRole: u.Allows(u.Permissions.Podcast), StreamRole: u.Allows(u.Permissions.Stream), JukeboxRole: u.Allows(u.Permissions.Jukebox), ShareRole: u.Allows(u.Permissions.Share), VideoConversionRole: u.Allows(u.Permissions.VideoConversion), } if fm, err := loadFolderMap(db); err == nil { allowedIDs, _ := database.AllowedMusicFolderIDs(db, u.ID) if len(allowedIDs) == 0 { for _, f := range fm.folders { n, _ := strconv.Atoi(fm.IntID(f.ID)) m.Folder = append(m.Folder, n) } } else { for _, id := range allowedIDs { n, _ := strconv.Atoi(fm.IntID(id)) m.Folder = append(m.Folder, n) } } } return m } func handleGetUser(rc *Context) *apiError { username, aerr := rc.Params.Required("username") if aerr != nil { return aerr } if username != rc.User.Username && !rc.User.IsAdmin { return errUnauthorized("viewing another user's account") } target, err := database.GetUserByUsername(rc.Server.DB, username) if err != nil { return errNotFound("User") } model := userToModel(rc, target) rc.Response = newResponse() rc.Response.User = &model return nil } func handleGetUsers(rc *Context) *apiError { if !rc.User.IsAdmin { return errUnauthorized("listing users") } users, err := database.ListUsers(rc.Server.DB) if err != nil { return newError(ErrGeneric, err.Error()) } out := &Users{} for _, u := range users { out.User = append(out.User, userToModel(rc, u)) } rc.Response = newResponse() rc.Response.Users = out return nil } // permissionsFromParams builds a Permissions set for a brand-new user: // any role param not present defaults to false (an explicit opt-in // model for account creation). func permissionsFromParams(p params) database.Permissions { get := func(name string) bool { return p.Bool(name, false) } return database.Permissions{ Settings: get("settingsRole"), Stream: get("streamRole"), Download: get("downloadRole"), Upload: get("uploadRole"), Playlist: get("playlistRole"), CoverArt: get("coverArtRole"), Comment: get("commentRole"), Podcast: get("podcastRole"), Share: get("shareRole"), Jukebox: get("jukeboxRole"), VideoConversion: get("videoConversionRole"), } } // mergePermissionsFromParams overlays only the role params actually // present in the request onto existing, so updateUser calls that omit // some roles (e.g. a client only toggling one checkbox) don't silently // revoke the rest. func mergePermissionsFromParams(p params, existing database.Permissions) database.Permissions { set := func(name string, cur bool) bool { if !p.Has(name) { return cur } return p.Bool(name, cur) } return database.Permissions{ Settings: set("settingsRole", existing.Settings), Stream: set("streamRole", existing.Stream), Download: set("downloadRole", existing.Download), Upload: set("uploadRole", existing.Upload), Playlist: set("playlistRole", existing.Playlist), CoverArt: set("coverArtRole", existing.CoverArt), Comment: set("commentRole", existing.Comment), Podcast: set("podcastRole", existing.Podcast), Share: set("shareRole", existing.Share), Jukebox: set("jukeboxRole", existing.Jukebox), VideoConversion: set("videoConversionRole", existing.VideoConversion), } } func decodePassword(raw string) string { if strings.HasPrefix(raw, "enc:") { if decoded, err := hex.DecodeString(strings.TrimPrefix(raw, "enc:")); err == nil { return string(decoded) } } return raw } func handleCreateUser(rc *Context) *apiError { if !rc.User.IsAdmin { return errUnauthorized("creating users") } username, aerr := rc.Params.Required("username") if aerr != nil { return aerr } rawPassword, aerr := rc.Params.Required("password") if aerr != nil { return aerr } email := rc.Params.Get("email") isAdmin := rc.Params.Bool("adminRole", false) user, err := database.CreateUser(rc.Server.DB, username, email, "", isAdmin) if err != nil { return newError(ErrGeneric, "could not create user: "+err.Error()) } if err := vsauth.SetUserPassword(rc.Server.DB, rc.Server.EncryptionKey, user.ID, decodePassword(rawPassword)); err != nil { return newError(ErrGeneric, err.Error()) } if err := database.SetUserPermissions(rc.Server.DB, user.ID, permissionsFromParams(rc.Params)); err != nil { return newError(ErrGeneric, err.Error()) } if folderIDs := rc.Params.GetAll("musicFolderId"); len(folderIDs) > 0 { if err := applyFolderRestriction(rc, user.ID, folderIDs); err != nil { return err } } rc.Response = newResponse() return nil } func applyFolderRestriction(rc *Context, userID string, folderIntIDs []string) *apiError { fm, err := loadFolderMap(rc.Server.DB) if err != nil { return newError(ErrGeneric, err.Error()) } var ids []string for _, s := range folderIntIDs { if f := fm.ByIntID(s); f != nil { ids = append(ids, f.ID) } } if err := database.SetAllowedMusicFolders(rc.Server.DB, userID, ids); err != nil { return newError(ErrGeneric, err.Error()) } return nil } func handleUpdateUser(rc *Context) *apiError { if !rc.User.IsAdmin { return errUnauthorized("modifying users") } username, aerr := rc.Params.Required("username") if aerr != nil { return aerr } target, err := database.GetUserByUsername(rc.Server.DB, username) if err != nil { return errNotFound("User") } if email := rc.Params.Get("email"); email != "" { if err := database.SetUserEmail(rc.Server.DB, target.ID, email); err != nil { return newError(ErrGeneric, err.Error()) } } if rc.Params.Has("adminRole") { if err := database.SetUserAdmin(rc.Server.DB, target.ID, rc.Params.Bool("adminRole", target.IsAdmin)); err != nil { return newError(ErrGeneric, err.Error()) } } if rc.Params.Has("password") { if err := vsauth.SetUserPassword(rc.Server.DB, rc.Server.EncryptionKey, target.ID, decodePassword(rc.Params.Get("password"))); err != nil { return newError(ErrGeneric, err.Error()) } } merged := mergePermissionsFromParams(rc.Params, target.Permissions) if err := database.SetUserPermissions(rc.Server.DB, target.ID, merged); err != nil { return newError(ErrGeneric, err.Error()) } if folderIDs := rc.Params.GetAll("musicFolderId"); len(folderIDs) > 0 { if aerr := applyFolderRestriction(rc, target.ID, folderIDs); aerr != nil { return aerr } } rc.Response = newResponse() return nil } func handleDeleteUser(rc *Context) *apiError { if !rc.User.IsAdmin { return errUnauthorized("deleting users") } username, aerr := rc.Params.Required("username") if aerr != nil { return aerr } target, err := database.GetUserByUsername(rc.Server.DB, username) if err != nil { return errNotFound("User") } if target.ID == rc.User.ID { return newError(ErrGeneric, "cannot delete your own account") } if err := database.DeleteUser(rc.Server.DB, target.ID); err != nil { return newError(ErrGeneric, err.Error()) } rc.Response = newResponse() return nil } func handleChangePassword(rc *Context) *apiError { username, aerr := rc.Params.Required("username") if aerr != nil { return aerr } rawPassword, aerr := rc.Params.Required("password") if aerr != nil { return aerr } if username != rc.User.Username && !rc.User.IsAdmin { return errUnauthorized("changing another user's password") } target, err := database.GetUserByUsername(rc.Server.DB, username) if err != nil { return errNotFound("User") } if err := vsauth.SetUserPassword(rc.Server.DB, rc.Server.EncryptionKey, target.ID, decodePassword(rawPassword)); err != nil { return newError(ErrGeneric, err.Error()) } rc.Response = newResponse() return nil }