fix(api): credit self-hosted gift codes to the issuing admin (#3119)

This commit is contained in:
Hampus
2026-10-02 17:23:26 +02:00
committed by GitHub
parent 5040ae2c10
commit 76e6891f5b
3 changed files with 22 additions and 3 deletions
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import {Config} from '@app/api/Config'; import {Config} from '@app/api/Config';
import {SYSTEM_USER_ID} from '@app/api/constants/Core';
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware'; import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware'; import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
@@ -42,6 +43,7 @@ export function CodesAdminController(app: HonoApp) {
count, count,
durationType: duration_type, durationType: duration_type,
durationQuantity: duration_quantity, durationQuantity: duration_quantity,
createdByUserId: Config.instance.selfHosted ? ctx.get('adminUserId') : SYSTEM_USER_ID,
}); });
await adminService.auditService.createAuditLog({ await adminService.auditService.createAuditLog({
adminUserId: ctx.get('adminUserId'), adminUserId: ctx.get('adminUserId'),
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import {SYSTEM_USER_ID} from '@app/api/constants/Core'; import type {UserID} from '@app/api/BrandedTypes';
import type {GiftCodeDurationType, GiftCodeRow} from '@app/api/database/types/PaymentTypes'; import type {GiftCodeDurationType, GiftCodeRow} from '@app/api/database/types/PaymentTypes';
import {mapGiftCodeDurationToMonths} from '@app/api/models/GiftCode'; import {mapGiftCodeDurationToMonths} from '@app/api/models/GiftCode';
import type {IUserRepository} from '@app/api/user/IUserRepository'; import type {IUserRepository} from '@app/api/user/IUserRepository';
@@ -15,13 +15,14 @@ interface GenerateGiftCodesOptions {
count: number; count: number;
durationType: GiftCodeDurationType; durationType: GiftCodeDurationType;
durationQuantity: number; durationQuantity: number;
createdByUserId: UserID;
} }
export class AdminCodeGenerationService { export class AdminCodeGenerationService {
constructor(private readonly userRepository: IUserRepository) {} constructor(private readonly userRepository: IUserRepository) {}
async generateGiftCodes(options: GenerateGiftCodesOptions): Promise<Array<string>> { async generateGiftCodes(options: GenerateGiftCodesOptions): Promise<Array<string>> {
const {count, durationType, durationQuantity} = this.validateOptions(options); const {count, durationType, durationQuantity, createdByUserId} = this.validateOptions(options);
const durationMonths = mapGiftCodeDurationToMonths(durationType, durationQuantity); const durationMonths = mapGiftCodeDurationToMonths(durationType, durationQuantity);
const codes: Array<string> = []; const codes: Array<string> = [];
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
@@ -32,7 +33,7 @@ export class AdminCodeGenerationService {
duration_type: durationType, duration_type: durationType,
duration_quantity: durationQuantity, duration_quantity: durationQuantity,
created_at: new Date(), created_at: new Date(),
created_by_user_id: SYSTEM_USER_ID, created_by_user_id: createdByUserId,
redeemed_at: null, redeemed_at: null,
redeemed_by_user_id: null, redeemed_by_user_id: null,
stripe_payment_intent_id: null, stripe_payment_intent_id: null,
@@ -284,6 +284,22 @@ describe('self-hosted premium routes', () => {
expect(gifts).toEqual([]); expect(gifts).toEqual([]);
}); });
test('credits admin generated gift codes to the issuing admin', async () => {
const admin = await setUserACLs(harness, await createTestAccount(harness), [
AdminACLs.AUTHENTICATE,
AdminACLs.GIFT_CODES_GENERATE,
]);
const {codes} = await createBuilder<{codes: Array<string>}>(harness, admin.token)
.post('/admin/gift-codes')
.body({count: 1, duration_type: 'months', duration_quantity: 1})
.execute();
const code = codes[0]?.split('/').pop();
const gift = await createBuilderWithoutAuth<{created_by: {id: string} | null}>(harness)
.get(`/gifts/${code}`)
.execute();
expect(gift.created_by?.id).toBe(admin.userId);
});
test('keeps purchase and hosted-only routes unavailable', async () => { test('keeps purchase and hosted-only routes unavailable', async () => {
const account = await createTestAccount(harness); const account = await createTestAccount(harness);
await expectRoutesNotFound(harness, account.token, [ await expectRoutesNotFound(harness, account.token, [