feat(auth): expose user permissions

This commit is contained in:
Quick
2026-05-26 18:13:32 -04:00
parent 24d48c5e6e
commit c95c40c3fe
3 changed files with 74 additions and 25 deletions
+68 -25
View File
@@ -112,19 +112,20 @@ func NewAdminHandler(
// createUserRequest represents the JSON body for POST /admin/users.
type createUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Role string `json:"role"`
CreateDefaultProfile bool `json:"create_default_profile"`
DefaultProfileName string `json:"default_profile_name,omitempty"`
LibraryIDs []int `json:"library_ids"`
MaxPlaybackQuality string `json:"max_playback_quality"`
MaxStreams *int `json:"max_streams,omitempty"`
MaxTranscodes *int `json:"max_transcodes,omitempty"`
MaxProfiles *int `json:"max_profiles,omitempty"`
DownloadAllowed *bool `json:"download_allowed,omitempty"`
DownloadTranscodeAllowed *bool `json:"download_transcode_allowed,omitempty"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
CreateDefaultProfile bool `json:"create_default_profile"`
DefaultProfileName string `json:"default_profile_name,omitempty"`
LibraryIDs []int `json:"library_ids"`
MaxPlaybackQuality string `json:"max_playback_quality"`
MaxStreams *int `json:"max_streams,omitempty"`
MaxTranscodes *int `json:"max_transcodes,omitempty"`
MaxProfiles *int `json:"max_profiles,omitempty"`
DownloadAllowed *bool `json:"download_allowed,omitempty"`
DownloadTranscodeAllowed *bool `json:"download_transcode_allowed,omitempty"`
}
type updateLibraryIDsField struct {
@@ -149,20 +150,43 @@ func (f updateLibraryIDsField) Ptr() *[]int {
return &value
}
type updateStringSliceField struct {
Set bool
Value []string
}
func (f *updateStringSliceField) UnmarshalJSON(data []byte) error {
f.Set = true
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
f.Value = []string{}
return nil
}
return json.Unmarshal(data, &f.Value)
}
func (f updateStringSliceField) Ptr() *[]string {
if !f.Set {
return nil
}
value := append([]string(nil), f.Value...)
return &value
}
// updateUserRequest represents the JSON body for PUT /admin/users/{id}.
type updateUserRequest struct {
Username *string `json:"username,omitempty"`
Email *string `json:"email,omitempty"`
Password *string `json:"password,omitempty"`
Role *string `json:"role,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
LibraryIDs updateLibraryIDsField `json:"library_ids,omitempty"`
MaxPlaybackQuality *string `json:"max_playback_quality,omitempty"`
MaxStreams *int `json:"max_streams,omitempty"`
MaxTranscodes *int `json:"max_transcodes,omitempty"`
MaxProfiles *int `json:"max_profiles,omitempty"`
DownloadAllowed *bool `json:"download_allowed,omitempty"`
DownloadTranscodeAllowed *bool `json:"download_transcode_allowed,omitempty"`
Username *string `json:"username,omitempty"`
Email *string `json:"email,omitempty"`
Password *string `json:"password,omitempty"`
Role *string `json:"role,omitempty"`
Permissions updateStringSliceField `json:"permissions,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
LibraryIDs updateLibraryIDsField `json:"library_ids,omitempty"`
MaxPlaybackQuality *string `json:"max_playback_quality,omitempty"`
MaxStreams *int `json:"max_streams,omitempty"`
MaxTranscodes *int `json:"max_transcodes,omitempty"`
MaxProfiles *int `json:"max_profiles,omitempty"`
DownloadAllowed *bool `json:"download_allowed,omitempty"`
DownloadTranscodeAllowed *bool `json:"download_transcode_allowed,omitempty"`
}
// adminUserResponse represents a user in admin JSON responses.
@@ -171,6 +195,7 @@ type adminUserResponse struct {
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
Enabled bool `json:"enabled"`
LibraryIDs []int `json:"library_ids"`
MaxPlaybackQuality string `json:"max_playback_quality"`
@@ -234,6 +259,7 @@ func toAdminUserResponse(u *models.User) adminUserResponse {
Username: u.Username,
Email: u.Email,
Role: u.Role,
Permissions: append([]string(nil), u.Permissions...),
Enabled: u.Enabled,
LibraryIDs: append([]int(nil), u.LibraryIDs...),
MaxPlaybackQuality: access.NormalizePlaybackQuality(u.MaxPlaybackQuality),
@@ -361,6 +387,11 @@ func (h *AdminHandler) HandleCreateUser(w http.ResponseWriter, r *http.Request)
writeError(w, http.StatusBadRequest, "bad_request", "max_profiles must be at least 1")
return
}
permissions, err := auth.NormalizePermissions(req.Permissions)
if err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
user, err := h.accountProvisioner.CreateAccount(r.Context(), auth.CreateAccountInput{
User: models.CreateUserInput{
@@ -368,6 +399,7 @@ func (h *AdminHandler) HandleCreateUser(w http.ResponseWriter, r *http.Request)
Email: req.Email,
Password: req.Password,
Role: req.Role,
Permissions: permissions,
LibraryIDs: req.LibraryIDs,
MaxPlaybackQuality: maxPlaybackQuality,
MaxStreams: req.MaxStreams,
@@ -418,12 +450,22 @@ func (h *AdminHandler) HandleUpdateUser(w http.ResponseWriter, r *http.Request)
writeError(w, http.StatusBadRequest, "bad_request", "max_profiles must be at least 1")
return
}
var permissions *[]string
if req.Permissions.Set {
normalized, err := auth.NormalizePermissions(req.Permissions.Value)
if err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
permissions = &normalized
}
err = h.userRepo.Update(r.Context(), id, models.UpdateUserInput{
Username: req.Username,
Email: req.Email,
Password: req.Password,
Role: req.Role,
Permissions: permissions,
Enabled: req.Enabled,
LibraryIDs: req.LibraryIDs.Ptr(),
MaxPlaybackQuality: maxPlaybackQuality,
@@ -710,6 +752,7 @@ func updateRequiresSessionRevocation(req updateUserRequest) bool {
req.Role != nil ||
req.Enabled != nil ||
req.LibraryIDs.Set ||
req.Permissions.Set ||
req.MaxPlaybackQuality != nil
}
+2
View File
@@ -106,6 +106,7 @@ type userResponse struct {
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
DownloadAllowed bool `json:"download_allowed"`
Impersonation *impersonationResponse `json:"impersonation,omitempty"`
}
@@ -510,6 +511,7 @@ func buildUserResponse(user *models.User, impersonatorUserID *int, impersonator
Username: user.Username,
Email: user.Email,
Role: user.Role,
Permissions: auth.EffectivePermissions(user),
DownloadAllowed: user.DownloadAllowed,
}
if impersonatorUserID != nil {
+4
View File
@@ -97,6 +97,7 @@ export interface User {
username: string;
email: string;
role: string;
permissions: string[];
download_allowed: boolean;
impersonation?: ImpersonationInfo | null;
}
@@ -1631,6 +1632,7 @@ export interface AdminUser {
username: string;
email: string;
role: string;
permissions: string[];
enabled: boolean;
library_ids: number[] | null;
max_playback_quality: string;
@@ -1649,6 +1651,7 @@ export interface CreateUserRequest {
email: string;
password: string;
role: string;
permissions?: string[];
create_default_profile?: boolean;
default_profile_name?: string;
library_ids?: number[] | null;
@@ -1665,6 +1668,7 @@ export interface UpdateUserRequest {
email?: string;
password?: string;
role?: string;
permissions?: string[];
enabled?: boolean;
library_ids?: number[] | null;
max_playback_quality?: string;