From 00bca1d47ebd0f63a7c5f07caaaf93d2b8187a30 Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Mon, 8 Jun 2026 11:11:56 -0700 Subject: [PATCH] =?UTF-8?q?Revert=20"feat=20(PUT-1091):=20have=20gui=20tok?= =?UTF-8?q?ens=20labeled=20and=20be=20different=20than=20reques=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 1335b46404a6bb94f5097b256b25c98f339c3d19. --- .../controllers/auth/AuthController.test.ts | 66 ----------- .../controllers/auth/AuthController.ts | 19 +-- .../services/appIcon/AppIconService.test.ts | 92 --------------- .../services/appIcon/AppIconService.ts | 15 +-- src/backend/services/auth/AuthService.test.ts | 111 ------------------ src/backend/services/auth/AuthService.ts | 37 +----- .../services/permission/PermissionService.ts | 26 ---- src/backend/services/permission/consts.ts | 16 --- src/backend/util/dbError.test.ts | 53 --------- src/backend/util/dbError.ts | 44 ------- src/gui/src/UI/Dashboard/TabAccount.js | 8 +- src/gui/src/UI/UIWindowAuthMe.js | 3 +- src/gui/src/UI/UIWindowCopyToken.js | 109 +++-------------- src/gui/src/helpers/create_access_token.js | 70 ----------- src/gui/src/i18n/translations/en.js | 20 ---- src/gui/src/initgui.js | 37 +----- 16 files changed, 30 insertions(+), 696 deletions(-) delete mode 100644 src/backend/services/appIcon/AppIconService.test.ts delete mode 100644 src/backend/util/dbError.test.ts delete mode 100644 src/backend/util/dbError.ts delete mode 100644 src/gui/src/helpers/create_access_token.js diff --git a/src/backend/controllers/auth/AuthController.test.ts b/src/backend/controllers/auth/AuthController.test.ts index ea650b137..a73fa9c30 100644 --- a/src/backend/controllers/auth/AuthController.test.ts +++ b/src/backend/controllers/auth/AuthController.test.ts @@ -39,7 +39,6 @@ import { runWithContext } from '../../core/context.js'; import { HttpError } from '../../core/http/HttpError.js'; import { requireUserActorGate } from '../../core/http/middleware/gates.js'; import { PuterServer } from '../../server.js'; -import { FULL_API_ACCESS } from '../../services/permission/consts.js'; import { setupTestServer } from '../../testUtil.js'; // ── Test harness ──────────────────────────────────────────────────── @@ -1288,71 +1287,6 @@ describe('AuthController.handleCreateAccessToken + handleRevokeAccessToken', () expect(decoded.user_uid).toBe(actor.user.uuid); }); - // -- Full-API-access + labels -- - - it('mints a full-access token and stores a trimmed label on the session row', async () => { - const res = makeRes(); - await controller.handleCreateAccessToken( - makeReq( - { permissions: [FULL_API_ACCESS], label: ' My CLI ' }, - { actor }, - ), - res, - ); - const decoded = server.services.token.verify( - 'auth', - (res.body as { token: string }).token, - ) as { type: string; token_uid: string; session_uid: string }; - expect(decoded.type).toBe('access-token'); - - const permRows = (await server.clients.db.read( - 'SELECT `permission` FROM `access_token_permissions` WHERE `token_uid` = ?', - [decoded.token_uid], - )) as Array<{ permission: string }>; - expect(permRows.map((r) => r.permission)).toContain(FULL_API_ACCESS); - - const sessRows = (await server.clients.db.read( - 'SELECT `label` FROM `sessions` WHERE `uuid` = ?', - [decoded.session_uid], - )) as Array<{ label: string }>; - expect(sessRows[0]?.label).toBe('My CLI'); - }); - - it('caps an over-long label at 64 characters', async () => { - const res = makeRes(); - await controller.handleCreateAccessToken( - makeReq( - { permissions: [FULL_API_ACCESS], label: 'x'.repeat(120) }, - { actor }, - ), - res, - ); - const decoded = server.services.token.verify( - 'auth', - (res.body as { token: string }).token, - ) as { session_uid: string }; - const sessRows = (await server.clients.db.read( - 'SELECT `label` FROM `sessions` WHERE `uuid` = ?', - [decoded.session_uid], - )) as Array<{ label: string }>; - expect(sessRows[0]?.label.length).toBe(64); - }); - - it('rejects a non-string label with 400', async () => { - await expect( - controller.handleCreateAccessToken( - makeReq( - { - permissions: [FULL_API_ACCESS], - label: 123 as unknown as string, - }, - { actor }, - ), - makeRes(), - ), - ).rejects.toMatchObject({ statusCode: 400 }); - }); - it('revoke-access-token: requires tokenOrUuid and returns ok:true on success', async () => { // Mint, then revoke. const created = makeRes(); diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index 7ec17c8f7..26d59c9fe 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -2212,25 +2212,13 @@ export class AuthController extends PuterController { requireAuth: true, }) async handleCreateAccessToken(req: Request, res: Response): Promise { - const { permissions, expiresIn, label } = req.body; + const { permissions, expiresIn } = req.body; if (!Array.isArray(permissions) || permissions.length === 0) { throw new HttpError(400, 'Missing or empty `permissions` array', { legacyCode: 'bad_request', }); } - // Optional user-facing name for the manage-sessions UI. Trim and clamp - // to the same 64-char limit the rename endpoint enforces. - let normalizedLabel: string | null = null; - if (label !== undefined && label !== null) { - if (typeof label !== 'string') { - throw new HttpError(400, '`label` must be a string', { - legacyCode: 'bad_request', - }); - } - normalizedLabel = label.trim().slice(0, 64) || null; - } - // Normalize specs: string → [string], [string] → [string, {}], [string, extra] → as-is const normalized = permissions.map((spec) => { if (typeof spec === 'string') return [spec]; @@ -2245,10 +2233,7 @@ export class AuthController extends PuterController { const token = await this.services.auth.createAccessToken( req.actor!, normalized as never, - { - ...(expiresIn ? { expiresIn } : {}), - ...(normalizedLabel ? { label: normalizedLabel } : {}), - }, + expiresIn ? { expiresIn } : {}, ); res.json({ token }); } diff --git a/src/backend/services/appIcon/AppIconService.test.ts b/src/backend/services/appIcon/AppIconService.test.ts deleted file mode 100644 index a25e72bc8..000000000 --- a/src/backend/services/appIcon/AppIconService.test.ts +++ /dev/null @@ -1,92 +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 { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import type { PuterServer } from '../../server'; -import type { IConfig } from '../../types'; -import { - POSTGRES_TEST_MIGRATIONS_PATH, - setupTestServer, -} from '../../testUtil.js'; - -const APP_ICONS_SUBDOMAIN = 'puter-app-icons'; - -describe('AppIconService.ensureIconsDirectory', () => { - let server: PuterServer; - - beforeAll(async () => { - // Boot on (pgmock) Postgres specifically: the `subdomains.subdomain` - // UNIQUE constraint this fix relies on exists on Postgres but not on - // the sqlite test schema, so only Postgres faithfully reproduces the - // duplicate-insert the race triggers. - // - // `no_default_user: false` provisions the admin user and the app-icons - // subdomain at boot — the realistic setup for the first-boot race. - server = await setupTestServer({ - no_default_user: false, - database: { - engine: 'postgres', - inMemory: true, - migrationPaths: [POSTGRES_TEST_MIGRATIONS_PATH], - }, - } as unknown as IConfig); - }, 180_000); // pgmock boot + migrations is slow - - afterAll(async () => { - await server?.shutdown(); - }, 60_000); - - // Regression: `ensureIconsDirectory` runs twice on first boot (once - // un-awaited from its own onServerStart, then from DefaultUserService), - // and the existence check reads a cache that can hold a stale negative - // entry. The losing create then violated the unique constraint and — on - // the un-awaited path — surfaced as an unhandled rejection. The "ensure" - // must be idempotent even when the guard wrongly reports "absent". - it('is idempotent when the existence cache reports the subdomain absent but the row exists', async () => { - const subdomains = server.stores.subdomain; - expect(await subdomains.existsBySubdomain(APP_ICONS_SUBDOMAIN)).toBe( - true, - ); - - // Reproduce the stale-negative-cache: write the negative marker for a - // subdomain that genuinely exists, so the guard in - // `ensureIconsDirectory` passes and it attempts a duplicate insert. - await server.clients.redis.set( - `subdomains:name:${APP_ICONS_SUBDOMAIN}`, - '__none__', - ); - // Self-check: confirm the poison took effect (guards against the - // cache key/marker drifting out from under this test). - expect(await subdomains.existsBySubdomain(APP_ICONS_SUBDOMAIN)).toBe( - false, - ); - - // Before the fix this rejected with a unique-constraint violation. - await expect( - server.services.appIcon.ensureIconsDirectory(), - ).resolves.toBeUndefined(); - - // And no duplicate row was created. - const rows = await server.clients.db.read( - 'SELECT COUNT(*) AS n FROM `subdomains` WHERE `subdomain` = ?', - [APP_ICONS_SUBDOMAIN], - ); - expect(Number(rows[0]?.n)).toBe(1); - }, 60_000); -}); diff --git a/src/backend/services/appIcon/AppIconService.ts b/src/backend/services/appIcon/AppIconService.ts index 6b289d412..526ad0a0d 100644 --- a/src/backend/services/appIcon/AppIconService.ts +++ b/src/backend/services/appIcon/AppIconService.ts @@ -19,7 +19,6 @@ import { Readable } from 'node:stream'; import type { LayerInstances } from '../../types'; -import { isUniqueViolation } from '../../util/dbError.js'; import type { puterServices } from '../index'; import { PuterService } from '../types.js'; @@ -199,25 +198,15 @@ export class AppIconService extends PuterService { } // Register the `puter-app-icons` subdomain pointing at that dir. - // Idempotent, and must stay so under a concurrent boot: this method - // runs twice on first boot — once un-awaited from our own - // `onServerStart`, then again (awaited) from `DefaultUserService` - // right after it creates the admin — and `existsBySubdomain` reads a - // cache that can still hold a stale negative entry. So the existence - // check can pass in both calls; let the unique constraint be the real - // arbiter and swallow the loser's duplicate. The end state (subdomain - // exists) is identical either way. + // Idempotent: skip if it already exists. const already = await this.stores.subdomain.existsBySubdomain(APP_ICONS_SUBDOMAIN); - if (already) return; - try { + if (!already) { await this.stores.subdomain.create({ userId: adminUser.id, subdomain: APP_ICONS_SUBDOMAIN, rootDirId: dirEntry.id ?? null, }); - } catch (e) { - if (!isUniqueViolation(e)) throw e; } } diff --git a/src/backend/services/auth/AuthService.test.ts b/src/backend/services/auth/AuthService.test.ts index e638e4b36..4e100a27a 100644 --- a/src/backend/services/auth/AuthService.test.ts +++ b/src/backend/services/auth/AuthService.test.ts @@ -24,7 +24,6 @@ import type { Actor } from '../../core/actor.js'; import { PuterServer } from '../../server.js'; import { setupTestServer } from '../../testUtil.js'; import { generateDefaultFsentries } from '../../util/userProvisioning.js'; -import { FULL_API_ACCESS } from '../permission/consts.js'; import { AuthService } from './AuthService.js'; function createAuthService(): AuthService { @@ -77,20 +76,6 @@ describe('AuthService.createAccessToken', () => { ), ).rejects.toMatchObject({ statusCode: 403 }); }); - - it('rejects a full-access mint by an app-under-user actor', async () => { - // Apps may hold scoped grants but must not be able to escalate to a - // blanket account-wide token. This throws on actor shape, before any - // DB / permission interaction, so the mock service is sufficient. - const authService = createAuthService(); - const appActor = { - user: { uuid: 'user-issuer', id: 1, username: 'issuer' }, - app: { id: 0, uid: 'app-x' }, - } as Actor; - await expect( - authService.createAccessToken(appActor, [[FULL_API_ACCESS]]), - ).rejects.toMatchObject({ statusCode: 403, legacyCode: 'forbidden' }); - }); }); // ── Real-server integration tests ─────────────────────────────────── @@ -1666,102 +1651,6 @@ describe('AuthService (integration)', () => { legacyCode: 'forbidden', }); }); - - // -- Full-API-access tokens -- - - it('mints a full-access token for a user actor and stores the label', async () => { - const user = await makeUser(); - const actor = { - user: { id: user.id, uuid: user.uuid, username: user.username }, - } as Actor; - const jwt = await authService.createAccessToken( - actor, - [[FULL_API_ACCESS]], - { label: 'My CLI' }, - ); - const decoded = server.services.token.verify('auth', jwt) as { - type: string; - token_uid: string; - session_uid: string; - }; - expect(decoded.type).toBe('access-token'); - - // The full-access grant is stored verbatim (no issuer-subset - // check rejects it). - const permRows = (await server.clients.db.read( - 'SELECT `permission` FROM `access_token_permissions` WHERE `token_uid` = ?', - [decoded.token_uid], - )) as Array<{ permission: string }>; - expect(permRows.map((r) => r.permission)).toContain( - FULL_API_ACCESS, - ); - - // The label lands on the access-token session row so it shows - // (and is revocable) in the manage-sessions UI. - const sessRows = (await server.clients.db.read( - 'SELECT `label`, `kind` FROM `sessions` WHERE `uuid` = ?', - [decoded.session_uid], - )) as Array<{ label: string; kind: string }>; - expect(sessRows[0]?.kind).toBe('access_token'); - expect(sessRows[0]?.label).toBe('My CLI'); - }); - - it('full-access token resolves any permission the issuing user holds, but a scoped token does not', async () => { - const user = await makeUser(); - await generateDefaultFsentries( - server.clients.db, - server.stores.user, - user, - ); - const body = Buffer.from('secret'); - await server.services.fs.write(user.id, { - fileMetadata: { - path: `/${user.username}/Documents/secret.txt`, - size: body.byteLength, - contentType: 'text/plain', - }, - fileContent: body, - } as never); - const fileEntry = await server.stores.fsEntry.getEntryByPath( - `/${user.username}/Documents/secret.txt`, - ); - expect(fileEntry).not.toBeNull(); - - const userActor = { - user: { id: user.id, uuid: user.uuid, username: user.username }, - } as Actor; - - // Full-access token: the owner fs:read resolves *through the - // issuer*, even though the token holds no fs grant of its own. - const fullJwt = await authService.createAccessToken(userActor, [ - [FULL_API_ACCESS], - ]); - const fullActor = - await authService.authenticateFromToken(fullJwt); - expect(fullActor).toBeTruthy(); - expect( - await server.services.permission.check( - fullActor!, - `fs:${fileEntry!.uuid}:read`, - ), - ).toBe(true); - - // A scoped token (granted an unrelated permission) gets NO owner - // fs access — access-token actors are excluded from the owner - // implicator, so this stays the pre-existing behaviour. - const scopedJwt = await authService.createAccessToken(userActor, [ - [`user:${user.uuid}:email:read`], - ]); - const scopedActor = - await authService.authenticateFromToken(scopedJwt); - expect(scopedActor).toBeTruthy(); - expect( - await server.services.permission.check( - scopedActor!, - `fs:${fileEntry!.uuid}:read`, - ), - ).toBe(false); - }); }); describe('private-asset / public hosted-actor tokens', () => { diff --git a/src/backend/services/auth/AuthService.ts b/src/backend/services/auth/AuthService.ts index 427427773..6e17d895b 100644 --- a/src/backend/services/auth/AuthService.ts +++ b/src/backend/services/auth/AuthService.ts @@ -30,7 +30,6 @@ import type { LayerInstances } from '../../types'; import { sessionCookieFlags } from '../../util/cookieFlags.js'; import type { puterServices } from '../index'; import { PuterService } from '../types'; -import { FULL_API_ACCESS } from '../permission/consts'; import { V1TokensDisabledError } from './TokenService'; import type { AccessTokenPayload, @@ -1442,7 +1441,7 @@ export class AuthService extends PuterService { // '30d'). `#hardExpiryFromExpiresIn` supports both, and existing // callers / tests pass the string form, so narrowing to `number` // here would force unsafe casts at every call site. - options: { expiresIn?: string | number; label?: string | null } = {}, + options: { expiresIn?: string | number } = {}, ): Promise { if (!actor.user) throw new HttpError(403, 'Actor must be a user', { @@ -1458,20 +1457,6 @@ export class AuthService extends PuterService { ); } - // Full-API-access sentinel: a token that may do anything its issuing - // user can do via the API (resolved against the issuer at check time — - // see PermissionService.#scanAccessToken). Only a plain user actor may - // mint one; an app-under-user actor must not be able to escalate the - // scoped access it was granted into blanket account-wide access. - const wantsFullAccess = permissions.some( - ([p]) => p === FULL_API_ACCESS, - ); - if (wantsFullAccess && actor.app) { - throw new HttpError(403, 'Apps may not mint full-access tokens', { - legacyCode: 'forbidden', - }); - } - // Permission-subset enforcement: an access token can only carry // permissions the issuer itself holds. Without this, an // app-under-user actor (third-party app authorized by the user) @@ -1480,19 +1465,12 @@ export class AuthService extends PuterService { // returned verbatim at check-time, with no re-validation against // the authorizer. `checkMany` is one pipelined MGET against the // per-actor permission cache so the cost is small even for - // many-permission mints. The full-access sentinel is excluded — it - // isn't a real permission the issuer "holds"; its gate is the - // user-actor check above. + // many-permission mints. const requestedPerms = [ ...new Set( permissions .map(([p]) => p) - .filter( - (p): p is string => - typeof p === 'string' && - !!p && - p !== FULL_API_ACCESS, - ), + .filter((p): p is string => typeof p === 'string' && !!p), ), ]; if (requestedPerms.length > 0) { @@ -1531,9 +1509,6 @@ export class AuthService extends PuterService { actor.user.id as number, { kind: 'access_token', - // User-facing name shown (and editable) in the manage-sessions - // UI. Trimmed/clamped by the caller; null when unnamed. - label: options.label ?? null, parent_session_id, expires_at: expiresAt, auth_id, @@ -1564,11 +1539,7 @@ export class AuthService extends PuterService { const jwt = this.services.token.sign( 'auth', jwtPayload, - // Only `expiresIn` is a valid jsonwebtoken sign option; `label` is - // ours (stored on the session row above), so don't forward it. - options.expiresIn !== undefined - ? { expiresIn: options.expiresIn as number } - : {}, + options as { expiresIn?: number }, ); // Store each permission grant diff --git a/src/backend/services/permission/PermissionService.ts b/src/backend/services/permission/PermissionService.ts index 670302ceb..b69dbba84 100644 --- a/src/backend/services/permission/PermissionService.ts +++ b/src/backend/services/permission/PermissionService.ts @@ -23,7 +23,6 @@ import { Context } from '../../core/context'; import { HttpError } from '../../core/http/HttpError.js'; import { PuterService } from '../types'; import { - FULL_API_ACCESS, MANAGE_PERM_PREFIX, PERMISSION_SCAN_CACHE_TTL_SECONDS, } from './consts'; @@ -432,31 +431,6 @@ export class PermissionService extends PuterService { ): Promise { if (!actor.accessToken) return; const issuerActor = actor.accessToken.issuer; - - // Full-API-access token: it may do anything its issuing user can do. - // Resolve every requested option directly against the issuer (the - // owner FS implicator, group/service grants, and user-to-user grants - // all apply to the user actor), with no per-permission grant row - // required. Account-management endpoints stay closed because they gate - // on actor type (requireUserActor / session cookie), not permissions. - const hasFullAccess = await this.stores.permission.hasAccessTokenPerm( - actor.accessToken.uid, - FULL_API_ACCESS, - ); - if (hasFullAccess) { - for (const permission of options) { - const issuerReading = await this.scan(issuerActor, permission); - reading.push({ - $: 'path', - via: 'access-token', - has_terminal: readingHasTerminal(issuerReading), - permission, - reading: issuerReading, - }); - } - return; - } - for (const permission of options) { const hasTokenPerm = await this.stores.permission.hasAccessTokenPerm( diff --git a/src/backend/services/permission/consts.ts b/src/backend/services/permission/consts.ts index 8b945d5dc..76e7263ef 100644 --- a/src/backend/services/permission/consts.ts +++ b/src/backend/services/permission/consts.ts @@ -29,19 +29,3 @@ export const PERMISSION_FOR_NOTHING_IN_PARTICULAR = /** TTL (seconds) for redis-cached permission scan readings. */ export const PERMISSION_SCAN_CACHE_TTL_SECONDS = 20; - -/** - * Sentinel grant for a "full API access" access token: the token may do - * anything its issuing user can do via the API (filesystem, drivers, KV, AI, - * apps, workers — resolved against the issuer at check time in - * `PermissionService.#scanAccessToken`). It does NOT unlock account - * management: access-token actors are still rejected by the - * `requireUserActor` / session-cookie gates that protect change-password, - * change-email, change-username, 2FA, sync-cookie, token minting, etc. - * - * Only a plain user actor may mint a token carrying this grant — never an - * app-under-user actor (which must not be able to escalate to full access). - * - * Chosen over `'*'`, which already appears as an audit-log "revoke all" label. - */ -export const FULL_API_ACCESS = 'full-api-access'; diff --git a/src/backend/util/dbError.test.ts b/src/backend/util/dbError.test.ts deleted file mode 100644 index 061600bc3..000000000 --- a/src/backend/util/dbError.test.ts +++ /dev/null @@ -1,53 +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 { describe, expect, it } from 'vitest'; -import { isUniqueViolation } from './dbError.js'; - -describe('isUniqueViolation', () => { - it('matches UNIQUE / PRIMARY KEY violations across the supported drivers', () => { - expect(isUniqueViolation({ code: '23505' })).toBe(true); // postgres - expect(isUniqueViolation({ code: 'SQLITE_CONSTRAINT_UNIQUE' })).toBe( - true, - ); - expect( - isUniqueViolation({ code: 'SQLITE_CONSTRAINT_PRIMARYKEY' }), - ).toBe(true); - expect(isUniqueViolation({ code: 'ER_DUP_ENTRY' })).toBe(true); // mysql - expect(isUniqueViolation({ errno: 1062 })).toBe(true); // mysql errno - }); - - it('does NOT match other constraint / error kinds, which must bubble up', () => { - // The bare parent code can be a CHECK / NOT NULL / FK violation. - expect(isUniqueViolation({ code: 'SQLITE_CONSTRAINT' })).toBe(false); - expect( - isUniqueViolation({ code: 'SQLITE_CONSTRAINT_FOREIGNKEY' }), - ).toBe(false); - expect(isUniqueViolation({ code: '23502' })).toBe(false); // pg NOT NULL - expect(isUniqueViolation({ code: '23503' })).toBe(false); // pg FK - expect(isUniqueViolation({ errno: 1452 })).toBe(false); // mysql FK - }); - - it('is safe on non-error inputs', () => { - expect(isUniqueViolation(null)).toBe(false); - expect(isUniqueViolation(undefined)).toBe(false); - expect(isUniqueViolation('boom')).toBe(false); - expect(isUniqueViolation({})).toBe(false); - }); -}); diff --git a/src/backend/util/dbError.ts b/src/backend/util/dbError.ts deleted file mode 100644 index 7b974fd15..000000000 --- a/src/backend/util/dbError.ts +++ /dev/null @@ -1,44 +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 . - */ - -/** - * True when `err` is a UNIQUE / PRIMARY KEY constraint violation, across the - * three database drivers Puter runs on. Use to make an "insert if absent" - * idempotent in the face of a lost check-then-insert race — only a duplicate - * is swallowed; CHECK / NOT NULL / FK / type violations still bubble up. - * - * better-sqlite3 surfaces `SqliteError.code`; mysql2 surfaces `.code` and - * `.errno` (1062 = ER_DUP_ENTRY); pg surfaces SQLSTATE `23505`. - */ -export function isUniqueViolation(err: unknown): boolean { - if (!err || typeof err !== 'object') return false; - const { code, errno } = err as { code?: string; errno?: number }; - // Match only UNIQUE / PRIMARY KEY violations. The bare `SQLITE_CONSTRAINT` - // parent code is intentionally excluded so CHECK / NOT NULL / FK / type - // violations still bubble up rather than being silently swallowed. - if ( - code === 'SQLITE_CONSTRAINT_UNIQUE' || - code === 'SQLITE_CONSTRAINT_PRIMARYKEY' || - code === 'ER_DUP_ENTRY' || - code === '23505' - ) { - return true; - } - return errno === 1062; -} diff --git a/src/gui/src/UI/Dashboard/TabAccount.js b/src/gui/src/UI/Dashboard/TabAccount.js index dc3b3cfba..9cd37b5cd 100644 --- a/src/gui/src/UI/Dashboard/TabAccount.js +++ b/src/gui/src/UI/Dashboard/TabAccount.js @@ -101,18 +101,18 @@ const TabAccount = { h += ''; } - // API token card + // Auth token card h += '
'; h += '
'; h += '
'; h += ''; h += '
'; h += '
'; - h += `${i18n('api_token')}`; - h += `${i18n('api_token_description')}`; + h += `${i18n('auth_token')}`; + h += `${i18n('copy_token_description')}`; h += '
'; h += '
'; - h += ``; + h += ``; h += '
'; // Danger zone diff --git a/src/gui/src/UI/UIWindowAuthMe.js b/src/gui/src/UI/UIWindowAuthMe.js index 8c35fe1be..6cf5c2d42 100644 --- a/src/gui/src/UI/UIWindowAuthMe.js +++ b/src/gui/src/UI/UIWindowAuthMe.js @@ -116,9 +116,8 @@ async function UIWindowAuthMe (options = {}) { h += ` `; - h += `${i18n('shared_api_token')}`; + h += `${i18n('your_auth_token')}`; h += ''; - h += `

${i18n('shared_api_token_note')}

`; h += ''; // Buttons diff --git a/src/gui/src/UI/UIWindowCopyToken.js b/src/gui/src/UI/UIWindowCopyToken.js index f0e80770e..0be4a377c 100644 --- a/src/gui/src/UI/UIWindowCopyToken.js +++ b/src/gui/src/UI/UIWindowCopyToken.js @@ -18,14 +18,7 @@ */ import UIWindow from './UIWindow.js'; -import UIWindowManageSessions from './UIWindowManageSessions.js'; -import create_access_token from '../helpers/create_access_token.js'; -// Creates a named, revocable full-API-access token and shows it once. -// Replaces the old "copy your raw GUI/session token" behaviour: the copied -// token used to be a session-equivalent credential that could escalate to -// full account control; the minted access token can use the whole API but is -// locked out of account management (see create_access_token.js). function UIWindowCopyToken (options = {}) { return new Promise(async (resolve) => { let h = ''; @@ -53,49 +46,29 @@ function UIWindowCopyToken (options = {}) { `; h += ''; - h += `

${i18n('create_api_token')}

`; - h += `

${i18n('create_token_message')}

`; + h += `

${i18n('auth_token')}

`; + h += `

${i18n('copy_token_message')}

`; h += ''; } h += '
'; + if ( ! options.show_header ) { + h += `
${i18n('copy_token_message')}
`; + } if ( options.show_close_button ) { h += '
×
'; } - - // -- Phase A: create form -- - h += '
'; - if ( ! options.show_header ) { - h += `
${i18n('create_token_message')}
`; - } - h += ''; - h += ``; - h += ``; - h += ``; - h += `'; - h += ``; - h += '
'; - - // -- Phase B: result (shown once, after mint) -- - h += '