Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions apps/api/src/auth/services/ability/ability.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,17 @@ import type { TemplateExecutor } from "lodash";
import template from "lodash/template";
import { singleton } from "tsyringe";

import { FeatureFlags, FeatureFlagValue } from "@src/core/services/feature-flags/feature-flags";
import type { FeatureFlagValue } from "@src/core/services/feature-flags/feature-flags";
import { FeatureFlagsService } from "@src/core/services/feature-flags/feature-flags.service";
import type { UserOutput } from "@src/user/repositories";

type Role = "REGULAR_USER" | "REGULAR_ANONYMOUS_USER" | "REGULAR_PAYING_USER" | "SUPER_USER";
type Role = "REGULAR_USER" | "REGULAR_PAYING_USER" | "SUPER_USER";

@singleton()
export class AbilityService {
readonly EMPTY_ABILITY = createMongoAbility([]);

private readonly RULES: Record<Role, Array<RawRule & { enabledIf?: FeatureFlagValue }>> = {
REGULAR_ANONYMOUS_USER: [
{ action: ["read", "sign"], subject: "UserWallet", conditions: { userId: "${user.id}" } },
{ action: "create", subject: "UserWallet", conditions: { userId: "${user.id}" }, enabledIf: FeatureFlags.ANONYMOUS_FREE_TRIAL },
{ action: "read", subject: "User", conditions: { id: "${user.id}" } },
{ action: "verify-email", subject: "User", conditions: { email: "${user.email}" } },
{ action: "manage", subject: "DeploymentSetting", conditions: { userId: "${user.id}" } }
],
REGULAR_USER: [
{ action: ["create", "read", "sign"], subject: "UserWallet", conditions: { userId: "${user.id}" } },
{ action: "manage", subject: "WalletSetting", conditions: { userId: "${user.id}" } },
Expand Down
14 changes: 0 additions & 14 deletions apps/api/src/auth/services/auth.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { container as globalContainer } from "tsyringe";

import { ApiKeyRepository } from "@src/auth/repositories/api-key/api-key.repository";
import { ApiKeyAuthService } from "@src/auth/services/api-key/api-key-auth.service";
import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.service";
import { ExecutionContextService } from "@src/core/services/execution-context/execution-context.service";
import type { UserOutput } from "@src/user/repositories/user/user.repository";
import { UserRepository } from "@src/user/repositories/user/user.repository";
Expand All @@ -16,10 +15,6 @@ import { AuthService } from "./auth.service";
import { UserSeeder } from "@test/seeders/user.seeder";

describe(AuthInterceptor.name, () => {
describe("Anonymous user", () => {
includeMarkUserAsActiveTests(() => UserSeeder.create({ userId: null }));
});

describe("Regular user", () => {
includeMarkUserAsActiveTests(() => UserSeeder.create());
});
Expand Down Expand Up @@ -99,21 +94,12 @@ describe(AuthInterceptor.name, () => {
di.registerInstance(
UserRepository,
mock<UserRepository>({
findAnonymousById: jest.fn().mockImplementation(async () => input?.user ?? UserSeeder.create()),
findByUserId: jest.fn().mockImplementation(async () => input?.user ?? UserSeeder.create()),
findById: jest.fn().mockImplementation(async () => input?.user ?? UserSeeder.create()),
markAsActive: jest.fn()
})
);
di.registerInstance(AuthService, mock());
di.registerInstance(
AuthTokenService,
mock<AuthTokenService>({
getValidUserId: jest.fn().mockImplementation(async () => {
return input?.apiKey && input?.user?.userId ? undefined : input?.user?.id;
})
})
);
di.registerInstance(
UserAuthTokenService,
mock<UserAuthTokenService>({
Expand Down
17 changes: 1 addition & 16 deletions apps/api/src/auth/services/auth.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { singleton } from "tsyringe";

import { AbilityService } from "@src/auth/services/ability/ability.service";
import { AuthService } from "@src/auth/services/auth.service";
import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.service";
import { ExecutionContextService } from "@src/core/services/execution-context/execution-context.service";
import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type";
import { UserOutput, UserRepository } from "@src/user/repositories";
Expand All @@ -30,7 +29,6 @@ export class AuthInterceptor implements HonoInterceptor {
private readonly abilityService: AbilityService,
private readonly userRepository: UserRepository,
private readonly authService: AuthService,
private readonly anonymousUserAuthService: AuthTokenService,
private readonly userAuthService: UserAuthTokenService,
private readonly apiKeyRepository: ApiKeyRepository,
private readonly apiKeyAuthService: ApiKeyAuthService,
Expand All @@ -41,15 +39,6 @@ export class AuthInterceptor implements HonoInterceptor {
return async (c: Context, next: Next) => {
const bearer = c.req.header("authorization");

const anonymousUserId = bearer && (await this.anonymousUserAuthService.getValidUserId(bearer));

if (anonymousUserId) {
const currentUser = await this.userRepository.findAnonymousById(anonymousUserId);
await this.auth(currentUser);
c.set("user", currentUser);
return await next();
}

const userId = bearer && (await this.userAuthService.getValidUserId(bearer, c.env));

if (userId) {
Expand Down Expand Up @@ -96,11 +85,7 @@ export class AuthInterceptor implements HonoInterceptor {
}

private getUserRole(user: UserOutput) {
if (user.userId) {
return user.trial === false ? "REGULAR_PAYING_USER" : "REGULAR_USER";
}

return "REGULAR_ANONYMOUS_USER";
return user.trial === false ? "REGULAR_PAYING_USER" : "REGULAR_USER";
}

private shouldMarkUserAsActive(userId: UserOutput["id"], now: Date): boolean {
Expand Down
83 changes: 39 additions & 44 deletions apps/api/src/billing/controllers/wallet/wallet.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import { RefillService } from "@src/billing/services/refill/refill.service";
import { StripeService } from "@src/billing/services/stripe/stripe.service";
import { StripeErrorService } from "@src/billing/services/stripe-error/stripe-error.service";
import { GetWalletOptions, WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import { FeatureFlags } from "@src/core/services/feature-flags/feature-flags";
import { FeatureFlagsService } from "@src/core/services/feature-flags/feature-flags.service";

@scoped(Lifecycle.ResolutionScoped)
export class WalletController {
Expand All @@ -29,58 +27,55 @@ export class WalletController {
private readonly authService: AuthService,
private readonly userWalletRepository: UserWalletRepository,
private readonly stripeService: StripeService,
private readonly stripeErrorService: StripeErrorService,
private readonly featureFlagsService: FeatureFlagsService
private readonly stripeErrorService: StripeErrorService
) {}

@Protected([{ action: "create", subject: "UserWallet" }])
async create({ data: { userId } }: StartTrialRequestInput): Promise<WalletOutputResponse> {
const { currentUser } = this.authService;

if (!this.featureFlagsService.isEnabled(FeatureFlags.ANONYMOUS_FREE_TRIAL)) {
assert(currentUser.emailVerified, 400, "Email not verified");
assert(currentUser.stripeCustomerId, 400, "Stripe customer ID not found");
assert(currentUser.emailVerified, 400, "Email not verified");
assert(currentUser.stripeCustomerId, 400, "Stripe customer ID not found");

const paymentMethods = await this.stripeService.getPaymentMethods(currentUser.id, currentUser.stripeCustomerId, this.authService.ability);
assert(paymentMethods.length > 0, 400, "You must have a payment method to start a trial.");
const paymentMethods = await this.stripeService.getPaymentMethods(currentUser.id, currentUser.stripeCustomerId, this.authService.ability);
assert(paymentMethods.length > 0, 400, "You must have a payment method to start a trial.");

if (this.stripeService.isProduction) {
const hasDuplicateTrialAccount = await this.stripeService.hasDuplicateTrialAccount(paymentMethods, currentUser.id);
assert(!hasDuplicateTrialAccount, 400, "This payment method is already associated with another trial account. Please use a different payment method.");
}
if (this.stripeService.isProduction) {
const hasDuplicateTrialAccount = await this.stripeService.hasDuplicateTrialAccount(paymentMethods, currentUser.id);
assert(!hasDuplicateTrialAccount, 400, "This payment method is already associated with another trial account. Please use a different payment method.");
}

const latestPaymentMethod = paymentMethods[0];
try {
const validationResult = await this.stripeService.validatePaymentMethodForTrial({
customer: currentUser.stripeCustomerId,
payment_method: latestPaymentMethod.id,
userId: currentUser.id
});

// If the card requires 3D Secure authentication, return the necessary information
if (validationResult.requires3DS) {
return {
data: {
id: null,
userId: currentUser.id,
address: null,
creditAmount: 0,
isTrialing: false,
createdAt: null,
requires3DS: true,
clientSecret: validationResult.clientSecret || null,
paymentIntentId: validationResult.paymentIntentId || null,
paymentMethodId: validationResult.paymentMethodId || null
}
};
}
} catch (error: unknown) {
if (this.stripeErrorService.isKnownError(error, "payment")) {
throw this.stripeErrorService.toAppError(error, "payment");
}

throw error;
const latestPaymentMethod = paymentMethods[0];
try {
const validationResult = await this.stripeService.validatePaymentMethodForTrial({
customer: currentUser.stripeCustomerId,
payment_method: latestPaymentMethod.id,
userId: currentUser.id
});

// If the card requires 3D Secure authentication, return the necessary information
if (validationResult.requires3DS) {
return {
data: {
id: null,
userId: currentUser.id,
address: null,
creditAmount: 0,
isTrialing: false,
createdAt: null,
requires3DS: true,
clientSecret: validationResult.clientSecret || null,
paymentIntentId: validationResult.paymentIntentId || null,
paymentMethodId: validationResult.paymentMethodId || null
}
};
}
} catch (error: unknown) {
if (this.stripeErrorService.isKnownError(error, "payment")) {
throw this.stripeErrorService.toAppError(error, "payment");
}

throw error;
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import { TxManagerService } from "@src/billing/services/tx-manager/tx-manager.service";
import { WalletReloadJobService } from "@src/billing/services/wallet-reload-job/wallet-reload-job.service";
import { DomainEventsService } from "@src/core/services/domain-events/domain-events.service";
import { FeatureFlags } from "@src/core/services/feature-flags/feature-flags";
import { FeatureFlagsService } from "@src/core/services/feature-flags/feature-flags.service";
import { UserOutput, UserRepository } from "@src/user/repositories";
import { BalancesService } from "../balances/balances.service";
import { ChainErrorService } from "../chain-error/chain-error.service";
Expand All @@ -36,7 +34,6 @@
private readonly authService: AuthService,
private readonly chainErrorService: ChainErrorService,
private readonly anonymousValidateService: TrialValidationService,
private readonly featureFlagsService: FeatureFlagsService,
private readonly txManagerService: TxManagerService,
private readonly domainEvents: DomainEventsService,
private readonly leaseHttpService: LeaseHttpService,
Expand Down Expand Up @@ -102,7 +99,7 @@
async executeDecodedTxByUserWallet(
userWallet: UserWalletOutput,
messages: EncodeObject[],
walletOwner?: UserOutput

Check failure on line 102 in apps/api/src/billing/services/managed-signer/managed-signer.service.ts

View workflow job for this annotation

GitHub Actions / validate / validate-app

'walletOwner' is defined but never used. Allowed unused args must match /^_/u
): Promise<{
code: number;
hash: string;
Expand All @@ -119,21 +116,8 @@

await this.anonymousValidateService.validateLeaseProvidersAuditors(messages, userWallet);

if (this.featureFlagsService.isEnabled(FeatureFlags.ANONYMOUS_FREE_TRIAL)) {
const user = walletOwner?.id === userWallet.userId ? walletOwner : await this.userRepository.findById(userWallet.userId!);
assert(user, 500, "User for wallet not found");
await Promise.all(
messages.map(message =>
Promise.all([
this.anonymousValidateService.validateLeaseProviders(message, userWallet, user),
this.anonymousValidateService.validateTrialLimit(message, userWallet)
])
)
);
}

const createLeaseMessage: { typeUrl: string; value: MsgCreateLease } | undefined = messages.find(message => message.typeUrl.endsWith(".MsgCreateLease"));
const hasCreateTrialLeaseMessage = userWallet.isTrialing && !!createLeaseMessage && !this.featureFlagsService.isEnabled(FeatureFlags.ANONYMOUS_FREE_TRIAL);
const hasCreateTrialLeaseMessage = userWallet.isTrialing && !!createLeaseMessage;
const hasLeases = hasCreateTrialLeaseMessage ? await this.leaseHttpService.hasLeases(userWallet.address!) : null;

const tx = await this.executeDerivedTx(userWallet.id, messages, userWallet.isOldWallet ?? false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { AuthService } from "@src/auth/services/auth.service";
import { TrialStarted } from "@src/billing/events/trial-started";
import { UserWalletPublicOutput, UserWalletRepository } from "@src/billing/repositories";
import { DomainEventsService } from "@src/core/services/domain-events/domain-events.service";
import { FeatureFlags } from "@src/core/services/feature-flags/feature-flags";
import { FeatureFlagsService } from "@src/core/services/feature-flags/feature-flags.service";
import { ManagedUserWalletService } from "../managed-user-wallet/managed-user-wallet.service";

@singleton()
Expand All @@ -14,8 +12,7 @@ export class WalletInitializerService {
private readonly walletManager: ManagedUserWalletService,
private readonly userWalletRepository: UserWalletRepository,
private readonly authService: AuthService,
private readonly domainEvents: DomainEventsService,
private readonly featureFlagsService: FeatureFlagsService
private readonly domainEvents: DomainEventsService
) {}

async initializeAndGrantTrialLimits(userId: string): Promise<UserWalletPublicOutput> {
Expand Down Expand Up @@ -43,7 +40,7 @@ export class WalletInitializerService {

const walletOutput = this.userWalletRepository.toPublic(userWallet);

if (isTrialSpendingAuthorized && !this.featureFlagsService.isEnabled(FeatureFlags.ANONYMOUS_FREE_TRIAL)) {
if (isTrialSpendingAuthorized) {
await this.domainEvents.publish(new TrialStarted({ userId }));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export const FeatureFlags = {
NOTIFICATIONS_ALERT_CREATE: "notifications_general_alerts_create",
NOTIFICATIONS_ALERT_UPDATE: "notifications_general_alerts_update",
ANONYMOUS_FREE_TRIAL: "anonymous_free_trial",
AUTO_CREDIT_RELOAD: "auto_credit_reload"
} as const;

Expand Down
4 changes: 1 addition & 3 deletions apps/api/src/rest-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import {
} from "./provider";
import { templatesRouter } from "./template";
import { transactionsRouter } from "./transaction";
import { createAnonymousUserRouter, getAnonymousUserRouter, getCurrentUserRouter, registerUserRouter } from "./user";
import { getCurrentUserRouter, registerUserRouter } from "./user";
import { validatorsRouter } from "./validator";

const appHono = new Hono<AppEnv>();
Expand Down Expand Up @@ -117,8 +117,6 @@ const openApiHonoHandlers: OpenApiHonoHandler[] = [
stripePaymentMethodsRouter,
stripeTransactionsRouter,
usageRouter,
createAnonymousUserRouter,
getAnonymousUserRouter,
registerUserRouter,
getCurrentUserRouter,
sendVerificationEmailRouter,
Expand Down
34 changes: 2 additions & 32 deletions apps/api/src/user/controllers/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@ import assert from "http-assert";
import { singleton } from "tsyringe";

import { AuthService, Protected } from "@src/auth/services/auth.service";
import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.service";
import { UserAuthTokenService } from "@src/auth/services/user-auth-token/user-auth-token.service";
import { ExecutionContextService } from "@src/core/services/execution-context/execution-context.service";
import { UserRepository } from "@src/user/repositories";
import type { GetUserParams } from "@src/user/routes/get-anonymous-user/get-anonymous-user.router";
import type { RegisterUserInput, RegisterUserResponse } from "@src/user/routes/register-user/register-user.router";
import { AnonymousUserResponseOutput, GetUserResponseOutput, UserSchema } from "@src/user/schemas/user.schema";
import { UserSchema } from "@src/user/schemas/user.schema";
import { UserService } from "@src/user/services/user/user.service";

@singleton()
export class UserController {
constructor(
private readonly userRepository: UserRepository,
private readonly authService: AuthService,
private readonly anonymousUserAuthService: AuthTokenService,
private readonly executionContextService: ExecutionContextService,
private readonly userService: UserService,
private readonly userAuthTokenService: UserAuthTokenService
Expand All @@ -27,35 +22,10 @@ export class UserController {
return this.executionContextService.get("HTTP_CONTEXT")!;
}

async create(): Promise<AnonymousUserResponseOutput> {
const user = await this.userRepository.create({
lastIp: this.httpContext.var.clientInfo?.ip,
lastUserAgent: this.httpContext.var.clientInfo?.userAgent,
lastFingerprint: this.httpContext.var.clientInfo?.fingerprint
});
return {
data: user,
token: this.anonymousUserAuthService.signTokenFor({ id: user.id })
};
}

@Protected([{ action: "read", subject: "User" }])
async getById({ id }: GetUserParams): Promise<{ data: UserSchema } | GetUserResponseOutput> {
const user = await this.userRepository.accessibleBy(this.authService.ability, "read").findById(id);

assert(user, 404);

return { data: user };
}

async registerUser(data: RegisterUserInput): Promise<RegisterUserResponse> {
const { req, env, var: httpVars } = this.httpContext;
const [userId, anonymousUserId] = await Promise.all([
this.userAuthTokenService.getValidUserId(req.header("authorization") || "", env),
this.anonymousUserAuthService.getValidUserId(req.header("x-anonymous-authorization") || "")
]);
const userId = await this.userAuthTokenService.getValidUserId(req.header("authorization") || "", env);
const user = await this.userService.registerUser({
anonymousUserId,
userId,
wantedUsername: data.wantedUsername,
email: data.email,
Expand Down
6 changes: 1 addition & 5 deletions apps/api/src/user/repositories/user/user.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, eq, isNull, lt, SQL, sql } from "drizzle-orm";
import { and, eq, lt, SQL, sql } from "drizzle-orm";
import { PgUpdateSetSource } from "drizzle-orm/pg-core";
import { singleton } from "tsyringe";

Expand Down Expand Up @@ -39,10 +39,6 @@ export class UserRepository extends BaseRepository<ApiPgTables["Users"], UserInp
return this.findUserWithWallet(eq(this.table.userId, userId!));
}

async findAnonymousById(id: UserOutput["id"]) {
return await this.cursor.query.Users.findFirst({ where: this.whereAccessibleBy(and(eq(this.table.id, id), isNull(this.table.userId))) });
}

async markAsActive(
id: UserOutput["id"],
options: {
Expand Down
Loading
Loading