mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
fix: patch app endpoints should allow partial fields (#1778)
* fix: update patch app endpoints to allow sending partial fields from the frontend * fix: removed unnessary complexity in handlesave func * fix: use correct subwallet app id when setting subwallet lightning address in get_info command * fix: do not allow changing subwallet to be non-isolated * chore: simplify update app scopes logic * fix: patch app to remove expires at --------- Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
parent
413b38d728
commit
faf80b122d
9 changed files with 160 additions and 109 deletions
211
api/api.go
211
api/api.go
|
|
@ -142,47 +142,57 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
|
|||
}
|
||||
|
||||
func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error {
|
||||
name := updateAppRequest.Name
|
||||
err := api.db.Transaction(func(tx *gorm.DB) error {
|
||||
// Initialize name with current app name, update if provided
|
||||
name := userApp.Name
|
||||
|
||||
if name == "" {
|
||||
return fmt.Errorf("won't update an app to have no name")
|
||||
}
|
||||
// Update app name if provided and different
|
||||
if updateAppRequest.Name != nil {
|
||||
name = *updateAppRequest.Name
|
||||
|
||||
maxAmount := updateAppRequest.MaxAmountSat
|
||||
budgetRenewal := updateAppRequest.BudgetRenewal
|
||||
|
||||
if len(updateAppRequest.Scopes) == 0 {
|
||||
return fmt.Errorf("won't update an app to have no request methods")
|
||||
}
|
||||
newScopes := updateAppRequest.Scopes
|
||||
|
||||
expiresAt, err := api.parseExpiresAt(updateAppRequest.ExpiresAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid expiresAt: %v", err)
|
||||
}
|
||||
|
||||
err = api.db.Transaction(func(tx *gorm.DB) error {
|
||||
// Update app name if it is not the same
|
||||
if name != userApp.Name {
|
||||
err := tx.Model(&db.App{}).Where("id", userApp.ID).Update("name", name).Error
|
||||
if err != nil {
|
||||
return err
|
||||
if name == "" {
|
||||
return fmt.Errorf("won't update an app to have no name")
|
||||
}
|
||||
if name != userApp.Name {
|
||||
err := tx.Model(&db.App{}).Where("id", userApp.ID).Update("name", name).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update app isolation if it is not the same
|
||||
if updateAppRequest.Isolated != userApp.Isolated {
|
||||
err := tx.Model(&db.App{}).Where("id", userApp.ID).Update("isolated", updateAppRequest.Isolated).Error
|
||||
if err != nil {
|
||||
return err
|
||||
// Update app isolation if provided and different
|
||||
if updateAppRequest.Isolated != nil {
|
||||
isolated := *updateAppRequest.Isolated
|
||||
if isolated != userApp.Isolated {
|
||||
if !isolated {
|
||||
var existingMetadata Metadata
|
||||
if userApp.Metadata != nil {
|
||||
err := json.Unmarshal(userApp.Metadata, &existingMetadata)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"app_id": userApp.ID,
|
||||
}).Error("Failed to deserialize app metadata")
|
||||
return err
|
||||
}
|
||||
if existingMetadata["app_store_app_id"] == constants.SUBWALLET_APPSTORE_APP_ID {
|
||||
return errors.New("Cannot update sub-wallet to be non-isolated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := tx.Model(&db.App{}).Where("id", userApp.ID).Update("isolated", isolated).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the app metadata
|
||||
// Update the app metadata if provided
|
||||
if updateAppRequest.Metadata != nil {
|
||||
var metadataBytes []byte
|
||||
var err error
|
||||
metadataBytes, err = json.Marshal(updateAppRequest.Metadata)
|
||||
metadataBytes, err = json.Marshal(*updateAppRequest.Metadata)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to serialize metadata")
|
||||
return err
|
||||
|
|
@ -193,54 +203,105 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
|
|||
}
|
||||
}
|
||||
|
||||
// Update existing permissions with new budget and expiry
|
||||
err = tx.Model(&db.AppPermission{}).Where("app_id", userApp.ID).Updates(map[string]interface{}{
|
||||
"ExpiresAt": expiresAt,
|
||||
"MaxAmountSat": maxAmount,
|
||||
"BudgetRenewal": budgetRenewal,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Handle permissions updates only if any permission-related field is provided
|
||||
if updateAppRequest.Scopes != nil || updateAppRequest.MaxAmountSat != nil ||
|
||||
updateAppRequest.BudgetRenewal != nil || updateAppRequest.ExpiresAt != nil || updateAppRequest.UpdateExpiresAt {
|
||||
|
||||
var existingPermissions []db.AppPermission
|
||||
if err := tx.Where("app_id = ?", userApp.ID).Find(&existingPermissions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Get current values or use provided ones
|
||||
var maxAmount uint64
|
||||
var budgetRenewal string
|
||||
var expiresAt *time.Time
|
||||
|
||||
existingScopeMap := make(map[string]bool)
|
||||
for _, perm := range existingPermissions {
|
||||
existingScopeMap[perm.Scope] = true
|
||||
}
|
||||
|
||||
if slices.Contains(newScopes, constants.SUPERUSER_SCOPE) && !existingScopeMap[constants.SUPERUSER_SCOPE] {
|
||||
return fmt.Errorf(
|
||||
"cannot update app to add superuser permission")
|
||||
}
|
||||
|
||||
// Add new permissions
|
||||
for _, scope := range newScopes {
|
||||
if !existingScopeMap[scope] {
|
||||
perm := db.AppPermission{
|
||||
App: *userApp,
|
||||
Scope: scope,
|
||||
ExpiresAt: expiresAt,
|
||||
MaxAmountSat: int(maxAmount),
|
||||
BudgetRenewal: budgetRenewal,
|
||||
}
|
||||
if err := tx.Create(&perm).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(existingScopeMap, scope)
|
||||
}
|
||||
|
||||
// Remove old permissions
|
||||
for scope := range existingScopeMap {
|
||||
if err := tx.Where("app_id = ? AND scope = ?", userApp.ID, scope).Delete(&db.AppPermission{}).Error; err != nil {
|
||||
// Get existing permissions to use as defaults
|
||||
var existingPermissions []db.AppPermission
|
||||
if err := tx.Where("app_id = ?", userApp.ID).Find(&existingPermissions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use existing values as defaults
|
||||
if len(existingPermissions) > 0 {
|
||||
// Find pay_invoice permission for budget-related fields
|
||||
for _, perm := range existingPermissions {
|
||||
if perm.Scope == constants.PAY_INVOICE_SCOPE {
|
||||
maxAmount = uint64(perm.MaxAmountSat)
|
||||
budgetRenewal = perm.BudgetRenewal
|
||||
expiresAt = perm.ExpiresAt
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override with provided values
|
||||
if updateAppRequest.MaxAmountSat != nil {
|
||||
maxAmount = *updateAppRequest.MaxAmountSat
|
||||
}
|
||||
if updateAppRequest.BudgetRenewal != nil {
|
||||
budgetRenewal = *updateAppRequest.BudgetRenewal
|
||||
}
|
||||
if updateAppRequest.ExpiresAt != nil {
|
||||
parsedExpiresAt, err := api.parseExpiresAt(*updateAppRequest.ExpiresAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid expiresAt: %v", err)
|
||||
}
|
||||
expiresAt = parsedExpiresAt
|
||||
}
|
||||
if updateAppRequest.ExpiresAt == nil && updateAppRequest.UpdateExpiresAt {
|
||||
expiresAt = nil
|
||||
}
|
||||
|
||||
// Update existing permissions with new budget and expiry
|
||||
err := tx.Model(&db.AppPermission{}).Where("app_id", userApp.ID).Updates(map[string]interface{}{
|
||||
"ExpiresAt": expiresAt,
|
||||
"MaxAmountSat": maxAmount,
|
||||
"BudgetRenewal": budgetRenewal,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle scope changes only if scopes were provided
|
||||
if updateAppRequest.Scopes != nil {
|
||||
|
||||
if len(updateAppRequest.Scopes) == 0 {
|
||||
return fmt.Errorf("won't update an app to have no request methods")
|
||||
}
|
||||
|
||||
existingScopeMap := make(map[string]bool)
|
||||
for _, perm := range existingPermissions {
|
||||
existingScopeMap[perm.Scope] = true
|
||||
}
|
||||
|
||||
if slices.Contains(updateAppRequest.Scopes, constants.SUPERUSER_SCOPE) && !existingScopeMap[constants.SUPERUSER_SCOPE] {
|
||||
return fmt.Errorf("cannot update app to add superuser permission")
|
||||
}
|
||||
|
||||
// Add new permissions
|
||||
for _, scope := range updateAppRequest.Scopes {
|
||||
if !existingScopeMap[scope] {
|
||||
perm := db.AppPermission{
|
||||
App: *userApp,
|
||||
Scope: scope,
|
||||
ExpiresAt: expiresAt,
|
||||
MaxAmountSat: int(maxAmount),
|
||||
BudgetRenewal: budgetRenewal,
|
||||
}
|
||||
if err := tx.Create(&perm).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(existingScopeMap, scope)
|
||||
}
|
||||
|
||||
// Remove old permissions
|
||||
for scope := range existingScopeMap {
|
||||
if err := tx.Where("app_id = ? AND scope = ?", userApp.ID, scope).Delete(&db.AppPermission{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish update event
|
||||
api.svc.GetEventPublisher().Publish(&events.Event{
|
||||
Event: "nwc_app_updated",
|
||||
Properties: map[string]interface{}{
|
||||
|
|
@ -425,9 +486,9 @@ func (api *api) ListApps(limit uint64, offset uint64, filters ListAppsFilters, o
|
|||
if filters.SubWallets != nil && !*filters.SubWallets {
|
||||
// exclude subwallets :scream:
|
||||
if api.db.Dialector.Name() == "sqlite" {
|
||||
query = query.Where("metadata is NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') IS NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') != ?", "uncle-jim")
|
||||
query = query.Where("metadata is NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') IS NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') != ?", constants.SUBWALLET_APPSTORE_APP_ID)
|
||||
} else {
|
||||
query = query.Where("metadata IS NULL OR metadata->>'app_store_app_id' IS NULL OR metadata->>'app_store_app_id' != ?", "uncle-jim")
|
||||
query = query.Where("metadata IS NULL OR metadata->>'app_store_app_id' IS NULL OR metadata->>'app_store_app_id' != ?", constants.SUBWALLET_APPSTORE_APP_ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,13 +116,14 @@ type ListAppsResponse struct {
|
|||
}
|
||||
|
||||
type UpdateAppRequest struct {
|
||||
Name string `json:"name"`
|
||||
MaxAmountSat uint64 `json:"maxAmount"`
|
||||
BudgetRenewal string `json:"budgetRenewal"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
Isolated bool `json:"isolated"`
|
||||
Name *string `json:"name"`
|
||||
MaxAmountSat *uint64 `json:"maxAmount"`
|
||||
BudgetRenewal *string `json:"budgetRenewal"`
|
||||
ExpiresAt *string `json:"expiresAt"`
|
||||
UpdateExpiresAt bool `json:"updateExpiresAt"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Metadata *Metadata `json:"metadata"`
|
||||
Isolated *bool `json:"isolated"`
|
||||
}
|
||||
|
||||
type TransferRequest struct {
|
||||
|
|
|
|||
|
|
@ -75,3 +75,5 @@ const (
|
|||
ENCRYPTION_TYPE_NIP04 = "nip04"
|
||||
ENCRYPTION_TYPE_NIP44_V2 = "nip44_v2"
|
||||
)
|
||||
|
||||
const SUBWALLET_APPSTORE_APP_ID = "uncle-jim"
|
||||
|
|
|
|||
|
|
@ -117,13 +117,8 @@ function SupportAlby() {
|
|||
}
|
||||
|
||||
// add the ZapPlanner subscription ID to the app metadata
|
||||
// Only send metadata since that's the only thing changing
|
||||
const updateAppRequest: UpdateAppRequest = {
|
||||
name: createAppRequest.name,
|
||||
scopes: createAppRequest.scopes,
|
||||
budgetRenewal: createAppRequest.budgetRenewal!,
|
||||
expiresAt: createAppRequest.expiresAt,
|
||||
maxAmount,
|
||||
isolated,
|
||||
metadata: {
|
||||
...createAppRequest.metadata,
|
||||
zapplanner_subscription_id: subscriptionId,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
|
|||
scopes: Array.from(permissions.scopes),
|
||||
budgetRenewal: permissions.budgetRenewal,
|
||||
expiresAt: permissions.expiresAt?.toISOString(),
|
||||
updateExpiresAt: true,
|
||||
maxAmount: permissions.maxAmount,
|
||||
isolated: permissions.isolated,
|
||||
};
|
||||
|
|
@ -155,13 +156,8 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
|
|||
|
||||
const handleConvertToSubwallet = async () => {
|
||||
try {
|
||||
// Only send the metadata since that's the only thing changing
|
||||
const updateAppRequest: UpdateAppRequest = {
|
||||
name: app.name,
|
||||
scopes: app.scopes,
|
||||
budgetRenewal: app.budgetRenewal,
|
||||
expiresAt: app.expiresAt,
|
||||
maxAmount: app.maxAmount,
|
||||
isolated: app.isolated,
|
||||
metadata: {
|
||||
...app.metadata,
|
||||
app_store_app_id: SUBWALLET_APPSTORE_APP_ID,
|
||||
|
|
|
|||
|
|
@ -316,13 +316,8 @@ export function ZapPlanner() {
|
|||
}
|
||||
|
||||
// add the ZapPlanner subscription ID to the app metadata
|
||||
// Only send metadata since that's the only thing changing
|
||||
const updateAppRequest: UpdateAppRequest = {
|
||||
name: createAppRequest.name,
|
||||
scopes: createAppRequest.scopes,
|
||||
budgetRenewal,
|
||||
expiresAt: createAppRequest.expiresAt,
|
||||
maxAmount,
|
||||
isolated,
|
||||
metadata: {
|
||||
...createAppRequest.metadata,
|
||||
zapplanner_subscription_id: subscriptionId,
|
||||
|
|
|
|||
|
|
@ -276,13 +276,14 @@ export interface CreateAppResponse {
|
|||
}
|
||||
|
||||
export type UpdateAppRequest = {
|
||||
name: string;
|
||||
maxAmount: number;
|
||||
budgetRenewal: string;
|
||||
expiresAt: string | undefined;
|
||||
scopes: Scope[];
|
||||
name?: string;
|
||||
maxAmount?: number;
|
||||
budgetRenewal?: string;
|
||||
expiresAt?: string | undefined;
|
||||
updateExpiresAt?: boolean;
|
||||
scopes?: Scope[];
|
||||
metadata?: AppMetadata;
|
||||
isolated: boolean;
|
||||
isolated?: boolean;
|
||||
};
|
||||
|
||||
export type Channel = {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ func (controller *nip47Controller) HandleGetInfoEvent(ctx context.Context, nip47
|
|||
if !app.Isolated {
|
||||
lightningAddress, _ := controller.albyOAuthService.GetLightningAddress()
|
||||
responsePayload.LightningAddress = &lightningAddress
|
||||
} else if metadata["app_store_app_id"] == "uncle_jim" && metadata["lud16"] != nil {
|
||||
} else if metadata["app_store_app_id"] == constants.SUBWALLET_APPSTORE_APP_ID && metadata["lud16"] != nil {
|
||||
lightningAddress := metadata["lud16"].(string)
|
||||
responsePayload.LightningAddress = &lightningAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ func TestHandleGetInfoEvent_SubwalletNoPermission(t *testing.T) {
|
|||
lightningAddress := "hello@getalby.com"
|
||||
|
||||
metadata := map[string]interface{}{
|
||||
"app_store_app_id": "uncle_jim",
|
||||
"app_store_app_id": constants.SUBWALLET_APPSTORE_APP_ID,
|
||||
"lud16": lightningAddress,
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ func TestHandleGetInfoEvent_SubwalletWithMetadata(t *testing.T) {
|
|||
lightningAddress := "hello@getalby.com"
|
||||
|
||||
metadata := map[string]interface{}{
|
||||
"app_store_app_id": "uncle_jim",
|
||||
"app_store_app_id": constants.SUBWALLET_APPSTORE_APP_ID,
|
||||
"lud16": lightningAddress,
|
||||
"a": 123,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue