diff --git a/src/backend/controllers/auth/AuthController.test.ts b/src/backend/controllers/auth/AuthController.test.ts
index ba047ccb7..50f4eb287 100644
--- a/src/backend/controllers/auth/AuthController.test.ts
+++ b/src/backend/controllers/auth/AuthController.test.ts
@@ -50,12 +50,10 @@ import {
import { PuterServer } from '../../server.js';
import { createSecret as otpCreateSecret } from '../../services/auth/OTPUtil.js';
import { kvSharePermission } from '../../services/kvShare/kvShares.js';
-import { FULL_API_ACCESS } from '../../services/permission/consts.js';
import {
- hasPushConsent,
+ FULL_API_ACCESS,
NOTIFICATIONS_PUSH_PERMISSION,
- type PushConsentDeps,
-} from '../../services/push/pushConsent.js';
+} from '../../services/permission/consts.js';
import { setupTestServer } from '../../testUtil.js';
import { resetCardVerificationStatusCache } from '../../util/cardFallback.js';
import { FS_READ_LIMIT } from '../fs/limits.js';
@@ -2184,7 +2182,7 @@ describe('AuthController grant flows', () => {
).toContain(permission);
});
- it('grant-user-app: grants an app push consent with notifications:push', async () => {
+ it('grant-user-app: grants and revokes notifications:push for an app', async () => {
const app = await server.stores.app.create(
{
name: `tgp-${uuidv4()}`,
@@ -2193,11 +2191,17 @@ describe('AuthController grant flows', () => {
},
{ ownerUserId: issuer.id },
);
- const deps = {
- stores: server.stores,
- services: { permission: server.services.permission },
- } as unknown as PushConsentDeps;
- expect(await hasPushConsent(deps, issuer.id, app.uid)).toBe(false);
+ const user = await server.stores.user.getById(issuer.id);
+ const appActor = makeActor({
+ user: user!,
+ app: { uid: app.uid, id: app.id },
+ });
+ const hasPush = () =>
+ server.services.permission.check(
+ appActor,
+ NOTIFICATIONS_PUSH_PERMISSION,
+ );
+ expect(await hasPush()).toBe(false);
// Anything past the bare permission names nothing a prompt describes.
await expect(
@@ -2217,7 +2221,7 @@ describe('AuthController grant flows', () => {
statusCode: 400,
legacyCode: 'permission_not_grantable',
});
- expect(await hasPushConsent(deps, issuer.id, app.uid)).toBe(false);
+ expect(await hasPush()).toBe(false);
const res = makeRes();
await inCtx(issuerActor, () =>
@@ -2233,7 +2237,21 @@ describe('AuthController grant flows', () => {
),
);
expect(res.body).toEqual({});
- expect(await hasPushConsent(deps, issuer.id, app.uid)).toBe(true);
+ expect(await hasPush()).toBe(true);
+
+ await inCtx(issuerActor, () =>
+ controller.handleRevokeUserApp(
+ makeReq(
+ {
+ app_uid: app.uid,
+ permission: NOTIFICATIONS_PUSH_PERMISSION,
+ },
+ { actor: issuerActor },
+ ),
+ makeRes(),
+ ),
+ );
+ expect(await hasPush()).toBe(false);
});
it('grant-user-app: resolves `origin` to the app when app_uid is omitted', async () => {
diff --git a/src/backend/services/permission/appGrantable.ts b/src/backend/services/permission/appGrantable.ts
index b952bcb93..b9399b03d 100644
--- a/src/backend/services/permission/appGrantable.ts
+++ b/src/backend/services/permission/appGrantable.ts
@@ -24,14 +24,13 @@ import {
type KvShareAccess,
kvShareRoot,
} from '../kvShare/kvShares.js';
-import { NOTIFICATIONS_PUSH_PERMISSION } from '../push/pushConsent.js';
import {
APP_DATA_CLASSES,
APP_DATA_KV_OPS,
APP_DATA_PERMISSION_PREFIX,
type AppDataStore,
} from './appDataScopes.js';
-import { MANAGE_PERM_PREFIX } from './consts.js';
+import { MANAGE_PERM_PREFIX, NOTIFICATIONS_PUSH_PERMISSION } from './consts.js';
import { PermissionUtil } from './permissionUtil.js';
const ACCESS_LEVELS = ['read', 'write'] as const;
diff --git a/src/backend/services/permission/consts.ts b/src/backend/services/permission/consts.ts
index f5fc9990c..d8f366535 100644
--- a/src/backend/services/permission/consts.ts
+++ b/src/backend/services/permission/consts.ts
@@ -20,6 +20,9 @@
export const MANAGE_PERM_PREFIX = 'manage';
export const PERM_KEY_PREFIX = 'perm';
+/** Per-app consent for an app's notifications to go out as pushes. */
+export const NOTIFICATIONS_PUSH_PERMISSION = 'notifications:push';
+
/**
* Width of the `permission` column in the permission tables, which every
* dialect declares as `varchar(255)`. Anything building a permission string
diff --git a/src/backend/services/push/pushConsent.test.ts b/src/backend/services/push/pushConsent.test.ts
deleted file mode 100644
index d2bb78ed5..000000000
--- a/src/backend/services/push/pushConsent.test.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright (C) 2024-present Puter Technologies Inc.
- *
- * This file is part of Puter.
- *
- * Puter is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-import { v4 as uuidv4 } from 'uuid';
-import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
-import { makeActor, type Actor } from '../../core/actor.js';
-import { PuterServer } from '../../server.js';
-import { setupTestServer } from '../../testUtil.js';
-import { EVENTS_BACKGROUND_PERMISSION } from '../events/authorization.js';
-import type { PermissionService } from '../permission/PermissionService.js';
-import {
- hasPushConsent,
- NOTIFICATIONS_PUSH_PERMISSION,
- type PushConsentDeps,
-} from './pushConsent.js';
-
-interface TestUser {
- id: number;
- uuid: string;
- username: string;
-}
-
-describe('hasPushConsent', () => {
- let server: PuterServer;
- let permission: PermissionService;
- let deps: PushConsentDeps;
-
- const makeUser = async (): Promise =>
- (await server.stores.user.create({
- username: `push${Math.random().toString(36).slice(2, 10)}`,
- uuid: uuidv4(),
- password: null,
- email: null,
- requires_email_confirmation: false,
- })) as TestUser;
-
- const makeApp = async (owner: TestUser): Promise => {
- const create = server.stores.app.create.bind(
- server.stores.app,
- ) as unknown as (
- fields: Record,
- opts: { ownerUserId: number },
- ) => Promise<{ uid: string }>;
- const app = await create(
- {
- name: `push-${uuidv4()}`,
- title: 'Push app',
- index_url: `https://push-${uuidv4()}.test/`,
- },
- { ownerUserId: owner.id },
- );
- return app.uid;
- };
-
- const userActor = (user: TestUser): Actor => makeActor({ user });
-
- const grant = (user: TestUser, appUid: string, perm: string) =>
- permission.grantUserAppPermission(userActor(user), appUid, perm);
-
- beforeAll(async () => {
- server = await setupTestServer();
- permission = server.services.permission as unknown as PermissionService;
- deps = {
- stores: server.stores,
- services: { permission },
- } as unknown as PushConsentDeps;
- }, 60_000);
-
- afterAll(async () => {
- await server?.shutdown();
- }, 60_000);
-
- it('is false for an app the user never granted, though the user holds the root', async () => {
- const user = await makeUser();
- const appUid = await makeApp(user);
-
- expect(
- await permission.check(
- userActor(user),
- NOTIFICATIONS_PUSH_PERMISSION,
- ),
- ).toBe(true);
- expect(await hasPushConsent(deps, user.id, appUid)).toBe(false);
- });
-
- it('is true only for the app and the user the grant names', async () => {
- const user = await makeUser();
- const other = await makeUser();
- const appUid = await makeApp(user);
- const otherAppUid = await makeApp(user);
-
- await grant(user, appUid, NOTIFICATIONS_PUSH_PERMISSION);
-
- expect(await hasPushConsent(deps, user.id, appUid)).toBe(true);
- expect(await hasPushConsent(deps, user.id, otherAppUid)).toBe(false);
- expect(await hasPushConsent(deps, other.id, appUid)).toBe(false);
- });
-
- it('turns false on the next check after the permission is revoked', async () => {
- const user = await makeUser();
- const appUid = await makeApp(user);
- await grant(user, appUid, NOTIFICATIONS_PUSH_PERMISSION);
- expect(await hasPushConsent(deps, user.id, appUid)).toBe(true);
-
- await permission.revokeUserAppPermission(
- userActor(user),
- appUid,
- NOTIFICATIONS_PUSH_PERMISSION,
- );
-
- expect(await hasPushConsent(deps, user.id, appUid)).toBe(false);
- });
-
- it('turns false on the next check after all of the app’s access is revoked', async () => {
- const user = await makeUser();
- const appUid = await makeApp(user);
- await grant(user, appUid, NOTIFICATIONS_PUSH_PERMISSION);
- expect(await hasPushConsent(deps, user.id, appUid)).toBe(true);
-
- await permission.revokeUserAppAll(userActor(user), appUid);
-
- expect(await hasPushConsent(deps, user.id, appUid)).toBe(false);
- });
-
- it('is not granted by events:background, nor grants it', async () => {
- const user = await makeUser();
- const backgroundApp = await makeApp(user);
- const pushApp = await makeApp(user);
- await grant(user, backgroundApp, EVENTS_BACKGROUND_PERMISSION);
- await grant(user, pushApp, NOTIFICATIONS_PUSH_PERMISSION);
-
- expect(await hasPushConsent(deps, user.id, backgroundApp)).toBe(false);
- const pushAppRow = await server.stores.app.getByUid(pushApp);
- expect(
- await permission.check(
- makeActor({ user, app: { uid: pushApp, id: pushAppRow.id } }),
- EVENTS_BACKGROUND_PERMISSION,
- ),
- ).toBe(false);
- });
-
- it('builds the actor from the stored app uid', async () => {
- const check = vi.fn(async () => true);
- const stub = {
- stores: {
- user: {
- getById: async () => ({ id: 1, uuid: 'u', username: 'u' }),
- },
- app: {
- getByUid: async () => ({ id: 7, uid: 'app-canonical' }),
- },
- },
- services: { permission: { check } },
- } as unknown as PushConsentDeps;
-
- expect(await hasPushConsent(stub, 1, 'APP-CANONICAL')).toBe(true);
- expect(check).toHaveBeenCalledWith(
- expect.objectContaining({ app: { uid: 'app-canonical', id: 7 } }),
- NOTIFICATIONS_PUSH_PERMISSION,
- );
- });
-
- it('is false for a user or an app that does not exist', async () => {
- const user = await makeUser();
- const appUid = await makeApp(user);
- await grant(user, appUid, NOTIFICATIONS_PUSH_PERMISSION);
-
- expect(await hasPushConsent(deps, user.id, `app-${uuidv4()}`)).toBe(
- false,
- );
- expect(await hasPushConsent(deps, 2_000_000_000, appUid)).toBe(false);
- });
-
- it('is false when the check itself fails', async () => {
- const user = await makeUser();
- const appUid = await makeApp(user);
- const failing = {
- ...deps,
- services: {
- permission: {
- check: async () => {
- throw new Error('unavailable');
- },
- },
- },
- } as PushConsentDeps;
-
- expect(await hasPushConsent(failing, user.id, appUid)).toBe(false);
- });
-});
diff --git a/src/backend/services/push/pushConsent.ts b/src/backend/services/push/pushConsent.ts
deleted file mode 100644
index 61b41d1a3..000000000
--- a/src/backend/services/push/pushConsent.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2024-present Puter Technologies Inc.
- *
- * This file is part of Puter.
- *
- * Puter is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-import { makeActor } from '../../core/actor.js';
-import type { UserRow } from '../../stores/user/UserStore.js';
-import type { PermissionService } from '../permission/PermissionService.js';
-
-/** Per-app consent for an app's notifications to go out as pushes. */
-export const NOTIFICATIONS_PUSH_PERMISSION = 'notifications:push';
-
-export interface PushConsentDeps {
- stores: {
- user: { getById: (id: number) => Promise };
- app: {
- getByUid: (
- uid: string,
- ) => Promise<
- { id?: number | null; uid?: string | null } | null | undefined
- >;
- };
- };
- services: { permission: Pick };
-}
-
-/** Read on every send for notifications an app itself sends, not Puter's. */
-export const hasPushConsent = async (
- deps: PushConsentDeps,
- userId: number,
- appUid: string,
-): Promise => {
- try {
- const [user, app] = await Promise.all([
- deps.stores.user.getById(userId),
- deps.stores.app.getByUid(appUid),
- ]);
- if (!user?.id || !app?.id || !app.uid) return false;
- // As the app for that user, keyed by the row's uid that revokes bump.
- const actor = makeActor({ user, app: { uid: app.uid, id: app.id } });
- return await deps.services.permission.check(
- actor,
- NOTIFICATIONS_PUSH_PERMISSION,
- );
- } catch (err) {
- console.warn('[push] consent check failed', err);
- return false;
- }
-};