diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index eaaefd4b2..2ab43f31c 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -78,9 +78,10 @@ if (BILLING_ENABLED === "true") { appHono.route("/", getWalletListRouter); appHono.route("/", signAndBroadcastTxRouter); // eslint-disable-next-line @typescript-eslint/no-var-requires - const { createAnonymousUserRouter, getAnonymousUserRouter } = require("./user"); + const { createAnonymousUserRouter, getAnonymousUserRouter, createOrGetUserRouter } = require("./user"); appHono.route("/", createAnonymousUserRouter); appHono.route("/", getAnonymousUserRouter); + appHono.route("/", createOrGetUserRouter); } appHono.get("/status", c => { diff --git a/apps/api/src/auth/services/ability/ability.service.ts b/apps/api/src/auth/services/ability/ability.service.ts index 321a27357..fa9bce93c 100644 --- a/apps/api/src/auth/services/ability/ability.service.ts +++ b/apps/api/src/auth/services/ability/ability.service.ts @@ -3,13 +3,14 @@ import type { TemplateExecutor } from "lodash"; import template from "lodash/template"; import { singleton } from "tsyringe"; -type Role = "REGULAR_USER" | "REGULAR_ANONYMOUS_USER" | "SUPER_USER"; +type Role = "REGULAR_UNREGISTERED_USER" | "REGULAR_USER" | "REGULAR_ANONYMOUS_USER" | "SUPER_USER"; @singleton() export class AbilityService { readonly EMPTY_ABILITY = new Ability([]); private readonly RULES: Record = { + REGULAR_UNREGISTERED_USER: [{ action: "create", subject: "User", conditions: { userId: "${user.userId}" } }], REGULAR_USER: [ { action: ["create", "read", "sign"], subject: "UserWallet", conditions: { userId: "${user.id}" } }, { action: "read", subject: "User", conditions: { id: "${user.id}" } } diff --git a/apps/api/src/auth/services/auth.interceptor.ts b/apps/api/src/auth/services/auth.interceptor.ts index 8146e2fab..dd8889fb4 100644 --- a/apps/api/src/auth/services/auth.interceptor.ts +++ b/apps/api/src/auth/services/auth.interceptor.ts @@ -22,6 +22,7 @@ export class AuthInterceptor implements HonoInterceptor { intercept() { return async (c: Context, next: Next) => { const bearer = c.req.header("authorization"); + const anonymousBearer = c.req.header("x-anonymous-authorization"); const anonymousUserId = bearer && (await this.anonymousUserAuthService.getValidUserId(bearer)); @@ -40,7 +41,9 @@ export class AuthInterceptor implements HonoInterceptor { const currentUser = await this.userRepository.findByUserId(userId); this.authService.currentUser = currentUser; - this.authService.ability = currentUser ? this.abilityService.getAbilityFor("REGULAR_USER", currentUser) : this.abilityService.EMPTY_ABILITY; + this.authService.ability = currentUser + ? this.abilityService.getAbilityFor("REGULAR_USER", currentUser) + : this.abilityService.getAbilityFor("REGULAR_UNREGISTERED_USER", { userId }); return await next(); } diff --git a/apps/api/src/auth/services/auth.service.ts b/apps/api/src/auth/services/auth.service.ts index a77efd66f..e2370130e 100644 --- a/apps/api/src/auth/services/auth.service.ts +++ b/apps/api/src/auth/services/auth.service.ts @@ -30,6 +30,7 @@ export class AuthService { } throwUnlessCan(action: string, subject: string) { + console.log("DEBUG this.ability", JSON.stringify(this.ability.rules, null, 2)); assert(this.ability.can(action, subject), 403); } } @@ -39,11 +40,11 @@ export const Protected = (rules?: { action: string; subject: string }[]) => (tar descriptor.value = function protectedFunction(...args: any[]) { const authService = container.resolve(AuthService); - - assert(authService.isAuthenticated, 401); - + console.log("DEBUG rules", JSON.stringify(rules, null, 2)); if (rules) { rules.forEach(rule => authService.throwUnlessCan(rule.action, rule.subject)); + } else { + assert(authService.isAuthenticated, 401); } return originalMethod.apply(this, args); diff --git a/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts index c79055ea1..d84283a71 100644 --- a/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts +++ b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts @@ -1,4 +1,4 @@ -import { and, eq, lte, or } from "drizzle-orm"; +import { eq, lte, or } from "drizzle-orm"; import first from "lodash/first"; import pick from "lodash/pick"; import { singleton } from "tsyringe"; @@ -8,7 +8,7 @@ import { ApiPgDatabase, InjectPg } from "@src/core/providers"; import { AbilityParams, BaseRepository } from "@src/core/repositories/base.repository"; import { TxService } from "@src/core/services"; -export type DbUserWalletInput = Partial; +export type DbUserWalletInput = Partial; export type UserWalletInput = Partial< Omit & { deploymentAllowance: number; @@ -27,18 +27,14 @@ export interface ListOptions { offset?: number; } -interface UpdateOptions { - returning: true; -} - @singleton() -export class UserWalletRepository extends BaseRepository { +export class UserWalletRepository extends BaseRepository { constructor( @InjectPg() protected readonly pg: ApiPgDatabase, @InjectUserWalletSchema() protected readonly schema: UserWalletSchema, protected readonly txManager: TxService ) { - super(pg, schema, txManager, "UserWallet"); + super(pg, schema, txManager, "UserWallet", "userWalletSchema"); } accessibleBy(...abilityParams: AbilityParams) { @@ -56,42 +52,6 @@ export class UserWalletRepository extends BaseRepository { return this.toOutput(first(await this.cursor.insert(this.schema).values(value).returning())); } - async updateById(id: UserWalletOutput["id"], payload: Partial, options?: UpdateOptions): Promise; - async updateById(id: UserWalletOutput["id"], payload: Partial): Promise; - async updateById(id: UserWalletOutput["id"], payload: Partial, options?: UpdateOptions): Promise { - return this.updateBy({ id }, payload, options); - } - - async updateBy(query: Partial, payload: Partial, options?: UpdateOptions): Promise; - async updateBy(query: Partial, payload: Partial): Promise; - async updateBy(query: Partial, payload: Partial, options?: UpdateOptions): Promise { - const cursor = this.cursor.update(this.schema).set(this.toInput(payload)).where(this.queryToWhere(query)); - - if (options?.returning) { - const items = await cursor.returning(); - return this.toOutput(first(items)); - } - - await cursor; - - return undefined; - } - - async find(query?: Partial) { - return this.toOutputList( - await this.cursor.query.userWalletSchema.findMany({ - where: this.queryToWhere(query) - }) - ); - } - - private queryToWhere(query: Partial) { - const fields = query && (Object.keys(query) as Array); - const where = fields?.length ? and(...fields.map(field => eq(this.schema[field], query[field]))) : undefined; - - return this.whereAccessibleBy(where); - } - async findDrainingWallets(thresholds = { fee: 0, deployment: 0 }, options?: Pick) { const where = or(lte(this.schema.deploymentAllowance, thresholds.deployment.toString()), lte(this.schema.feeAllowance, thresholds.fee.toString())); @@ -103,19 +63,11 @@ export class UserWalletRepository extends BaseRepository { ); } - async findById(id: UserWalletOutput["id"]) { - return this.toOutput(await this.cursor.query.userWalletSchema.findFirst({ where: this.whereAccessibleBy(eq(this.schema.id, id)) })); - } - async findByUserId(userId: UserWalletOutput["userId"]) { return this.toOutput(await this.cursor.query.userWalletSchema.findFirst({ where: this.whereAccessibleBy(eq(this.schema.userId, userId)) })); } - private toOutputList(dbOutput: UserWalletSchema["$inferSelect"][]): UserWalletOutput[] { - return dbOutput.map(item => this.toOutput(item)); - } - - private toOutput(dbOutput?: UserWalletSchema["$inferSelect"]): UserWalletOutput { + protected toOutput(dbOutput: DbUserWalletOutput): UserWalletOutput { return ( dbOutput && { ...dbOutput, @@ -126,7 +78,7 @@ export class UserWalletRepository extends BaseRepository { ); } - private toInput({ deploymentAllowance, feeAllowance, ...input }: UserWalletInput): DbUserWalletInput { + protected toInput({ deploymentAllowance, feeAllowance, ...input }: UserWalletInput): DbUserWalletInput { const dbInput: DbUserWalletInput = { ...input, updatedAt: new Date() diff --git a/apps/api/src/core/repositories/base.repository.ts b/apps/api/src/core/repositories/base.repository.ts index a02bc6be4..e7e2ad326 100644 --- a/apps/api/src/core/repositories/base.repository.ts +++ b/apps/api/src/core/repositories/base.repository.ts @@ -1,6 +1,8 @@ import { AnyAbility } from "@casl/ability"; +import { and, eq } from "drizzle-orm"; import { PgTableWithColumns } from "drizzle-orm/pg-core/table"; import { SQL } from "drizzle-orm/sql/sql"; +import first from "lodash/first"; import { ApiPgDatabase, InjectPg, TxService } from "@src/core"; import { DrizzleAbility } from "@src/lib/drizzle-ability/drizzle-ability"; @@ -8,18 +10,39 @@ import { InjectUserSchema } from "@src/user/providers"; export type AbilityParams = [AnyAbility, Parameters[0]]; -export abstract class BaseRepository> { +interface UpdateOptions { + returning: true; +} + +export interface BaseRecordInput { + id?: T; +} + +export interface BaseRecordOutput { + id: T; +} + +export abstract class BaseRepository< + T extends PgTableWithColumns, + Input extends BaseRecordInput, + Output extends BaseRecordOutput +> { protected ability?: DrizzleAbility; get cursor() { return this.txManager.getPgTx() || this.pg; } + get queryCursor() { + return this.cursor.query[this.schemaName] as unknown as T; + } + constructor( @InjectPg() protected readonly pg: ApiPgDatabase, @InjectUserSchema() protected readonly schema: T, protected readonly txManager: TxService, - protected readonly entityName: string + protected readonly entityName: string, + protected readonly schemaName: keyof ApiPgDatabase["query"] ) {} protected withAbility(ability: AnyAbility, action: Parameters[0]) { @@ -32,4 +55,64 @@ export abstract class BaseRepository> { } abstract accessibleBy(...abilityParams: AbilityParams): this; + + async findById(id: Output["id"]) { + return this.toOutput(await this.queryCursor.findFirst({ where: this.whereAccessibleBy(eq(this.schema.id, id)) })); + } + + async findOneBy(query?: Partial) { + return this.toOutput( + await this.queryCursor.findFirst({ + where: this.queryToWhere(query) + }) + ); + } + + async find(query?: Partial) { + return this.toOutputList( + await this.queryCursor.findMany({ + where: this.queryToWhere(query) + }) + ); + } + + async updateById(id: Output["id"], payload: Partial, options?: UpdateOptions): Promise; + async updateById(id: Output["id"], payload: Partial): Promise; + async updateById(id: Output["id"], payload: Partial, options?: UpdateOptions): Promise { + return this.updateBy({ id } as Partial, payload, options); + } + + async updateBy(query: Partial, payload: Partial, options?: UpdateOptions): Promise; + async updateBy(query: Partial, payload: Partial): Promise; + async updateBy(query: Partial, payload: Partial, options?: UpdateOptions): Promise { + const cursor = this.cursor.update(this.schema).set(this.toInput(payload)).where(this.queryToWhere(query)); + + if (options?.returning) { + const items = await cursor.returning(); + return this.toOutput(first(items)); + } + + await cursor; + + return undefined; + } + + protected queryToWhere(query: Partial) { + const fields = query && (Object.keys(query) as Array); + const where = fields?.length ? and(...fields.map(field => eq(this.schema[field], query[field]))) : undefined; + + return this.whereAccessibleBy(where); + } + + protected toInput(payload: Partial): Partial { + return payload as Partial; + } + + protected toOutputList(dbOutput: T["$inferSelect"][]): Output[] { + return dbOutput.map(item => this.toOutput(item)); + } + + protected toOutput(payload: Partial): Output { + return payload as Output; + } } diff --git a/apps/api/src/user/controllers/user/user.controller.ts b/apps/api/src/user/controllers/user/user.controller.ts index 3d74e06f5..a43fbc334 100644 --- a/apps/api/src/user/controllers/user/user.controller.ts +++ b/apps/api/src/user/controllers/user/user.controller.ts @@ -4,18 +4,21 @@ 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 { UserRepository } from "@src/user/repositories"; +import { UserCreateRequestInput, UserResponseOutput } from "@src/user/routes/create-or-get-user/create-or-get-user.router"; import { GetUserParams } from "@src/user/routes/get-anonymous-user/get-anonymous-user.router"; import { AnonymousUserResponseOutput } from "@src/user/schemas/user.schema"; +import { UserInitInput, UserInitService } from "@src/user/services/user-init/user-init.service"; @singleton() export class UserController { constructor( private readonly userRepository: UserRepository, private readonly authService: AuthService, - private readonly anonymousUserAuthService: AuthTokenService + private readonly anonymousUserAuthService: AuthTokenService, + private readonly userService: UserInitService ) {} - async create(): Promise { + async createAnonymous(): Promise { const user = await this.userRepository.create(); return { data: user, @@ -23,6 +26,12 @@ export class UserController { }; } + async createOrGet(input: UserCreateRequestInput): Promise { + return { + data: await this.userService.registerOrGetUser(input.data as UserInitInput) + }; + } + @Protected([{ action: "read", subject: "User" }]) async getById({ id }: GetUserParams): Promise { const user = await this.userRepository.accessibleBy(this.authService.ability, "read").findById(id); diff --git a/apps/api/src/user/repositories/user/user.repository.ts b/apps/api/src/user/repositories/user/user.repository.ts index 0ec67c1ae..f5d6e9f96 100644 --- a/apps/api/src/user/repositories/user/user.repository.ts +++ b/apps/api/src/user/repositories/user/user.repository.ts @@ -8,33 +8,30 @@ import { TxService } from "@src/core/services"; import { InjectUserSchema, UserSchema } from "@src/user/providers"; export type UserOutput = UserSchema["$inferSelect"]; +export type UserInput = Partial; @singleton() -export class UserRepository extends BaseRepository { +export class UserRepository extends BaseRepository { constructor( @InjectPg() protected readonly pg: ApiPgDatabase, @InjectUserSchema() protected readonly schema: UserSchema, protected readonly txManager: TxService ) { - super(pg, schema, txManager, "User"); + super(pg, schema, txManager, "User", "userSchema"); } accessibleBy(...abilityParams: AbilityParams) { return new UserRepository(this.pg, this.schema, this.txManager).withAbility(...abilityParams) as this; } - async create() { - return first(await this.cursor.insert(this.schema).values({}).returning({ id: this.schema.id })); + async create(input: Partial = {}) { + return first(await this.cursor.insert(this.schema).values(input).returning()); } async findByUserId(userId: UserOutput["userId"]) { return await this.cursor.query.userSchema.findFirst({ where: this.whereAccessibleBy(eq(this.schema.userId, userId)) }); } - async findById(id: UserOutput["id"]) { - return await this.cursor.query.userSchema.findFirst({ where: this.whereAccessibleBy(eq(this.schema.id, id)) }); - } - async findAnonymousById(id: UserOutput["id"]) { return await this.cursor.query.userSchema.findFirst({ where: this.whereAccessibleBy(and(eq(this.schema.id, id), isNull(this.schema.userId))) }); } diff --git a/apps/api/src/user/routes/create-anonymous-user/create-anonymous-user.router.ts b/apps/api/src/user/routes/create-anonymous-user/create-anonymous-user.router.ts index 6e692b06a..5a1a54a6a 100644 --- a/apps/api/src/user/routes/create-anonymous-user/create-anonymous-user.router.ts +++ b/apps/api/src/user/routes/create-anonymous-user/create-anonymous-user.router.ts @@ -26,5 +26,5 @@ const route = createRoute({ export const createAnonymousUserRouter = new OpenAPIHono(); createAnonymousUserRouter.openapi(route, async function routeCreateUser(c) { - return c.json(await container.resolve(UserController).create(), 200); + return c.json(await container.resolve(UserController).createAnonymous(), 200); }); diff --git a/apps/api/src/user/routes/create-or-get-user/create-or-get-user.router.ts b/apps/api/src/user/routes/create-or-get-user/create-or-get-user.router.ts new file mode 100644 index 000000000..4c5a333b6 --- /dev/null +++ b/apps/api/src/user/routes/create-or-get-user/create-or-get-user.router.ts @@ -0,0 +1,66 @@ +import { createRoute, OpenAPIHono } from "@hono/zod-openapi"; +import { container } from "tsyringe"; +import { z } from "zod"; + +import { UserController } from "@src/user/controllers/user/user.controller"; + +const UserCreateRequestInputSchema = z.object({ + data: z.object({ + userId: z.string(), + wantedUsername: z.string(), + email: z.string().email(), + emailVerified: z.boolean(), + subscribedToNewsletter: z.boolean() + }) +}); +const UserResponseOutputSchema = z.object({ + data: z.object({ + id: z.string().uuid(), + userId: z.string(), + username: z.string(), + email: z.string().email(), + emailVerified: z.boolean(), + bio: z.string(), + subscribedToNewsletter: z.boolean(), + youtubeUsername: z.string(), + twitterUsername: z.string(), + githubUsername: z.string() + }) +}); + +export type UserCreateRequestInput = z.infer; +export type UserResponseOutput = z.infer; + +const route = createRoute({ + method: "post", + path: "/v1/users", + summary: "Creates an anonymous user", + tags: ["Users"], + request: { + body: { + description: "", + content: { + "application/json": { + schema: UserCreateRequestInputSchema + } + } + } + }, + responses: { + 200: { + description: "Returns a created anonymous user", + body: { + content: { + "application/json": { + schema: UserResponseOutputSchema + } + } + } + } + } +}); +export const createOrGetUserRouter = new OpenAPIHono(); + +createOrGetUserRouter.openapi(route, async function routeCreateUser(c) { + return c.json(await container.resolve(UserController).createOrGet(c.req.valid("json")), 200); +}); diff --git a/apps/api/src/user/routes/index.ts b/apps/api/src/user/routes/index.ts index 947699f90..1211388a0 100644 --- a/apps/api/src/user/routes/index.ts +++ b/apps/api/src/user/routes/index.ts @@ -1,2 +1,3 @@ export * from "@src/user/routes/create-anonymous-user/create-anonymous-user.router"; export * from "@src/user/routes/get-anonymous-user/get-anonymous-user.router"; +export * from "@src/user/routes/create-or-get-user/create-or-get-user.router"; diff --git a/apps/api/src/user/services/user-init/user-init.service.ts b/apps/api/src/user/services/user-init/user-init.service.ts new file mode 100644 index 000000000..da3682035 --- /dev/null +++ b/apps/api/src/user/services/user-init/user-init.service.ts @@ -0,0 +1,174 @@ +import pick from "lodash/pick"; +import { Transaction } from "sequelize"; +import { singleton } from "tsyringe"; + +import { AuthService } from "@src/auth/services/auth.service"; +import { UserWalletRepository } from "@src/billing/repositories"; +import { LoggerService } from "@src/core"; +import { checkUsernameAvailable } from "@src/services/db/userDataService"; +import { UserOutput, UserRepository } from "@src/user/repositories"; + +export type UserInitInput = { + anonymousUserId?: string; + userId: string; + wantedUsername: string; + email: string; + emailVerified: boolean; + subscribedToNewsletter: boolean; +}; + +@singleton() +export class UserInitService { + private readonly logger = new LoggerService({ context: UserInitService.name }); + constructor( + private readonly userRepository: UserRepository, + private readonly userWalletRepository: UserWalletRepository, + private readonly authService: AuthService + ) {} + + async registerOrGetUser(input: UserInitInput) { + let user = await this.tryToRegisterAnonymousUser(input); + + if (!user) { + user = await this.tryToRetrieveUser(input); + } + + if (user && input.anonymousUserId) { + await this.tryToTransferWallet(input.anonymousUserId, user.id); + } else { + user = await this.tryToRegisterUser(input); + } + + user = await this.tryToUpdateUser(input, user); + + return pick(user, [ + "id", + "userId", + "username", + "email", + "emailVerified", + "stripeCustomerId", + "bio", + "subscribedToNewsletter", + "youtubeUsername", + "twitterUsername", + "githubUsername" + ]); + } + + private async tryToRegisterAnonymousUser(input: UserInitInput): Promise { + if (!input.anonymousUserId) { + return; + } + try { + const user = await this.userRepository.updateBy( + { id: input.anonymousUserId, userId: null }, + { + userId: input.userId, + username: await this.generateUsername(input.wantedUsername), + email: input.email, + emailVerified: input.emailVerified, + stripeCustomerId: null, + subscribedToNewsletter: input.subscribedToNewsletter + } + ); + + if (user) { + this.logger.info({ event: "ANONYMOUS_USER_REGISTERED", id: input.anonymousUserId, userId: input.userId }); + } + + return user; + } catch (error) { + if (error.name !== "SequelizeUniqueConstraintError") { + throw error; + } + + this.logger.info({ event: "ANONYMOUS_USER_ALREADY_REGISTERED", id: input.anonymousUserId, userId: input.userId }); + } + } + + private async tryToRetrieveUser(input: UserInitInput): Promise { + const user = await this.userRepository.findOneBy({ userId: input.userId }); + + if (user) { + this.logger.debug({ event: "USER_RETRIEVED", userId: input.userId }); + } + + if (user && input.anonymousUserId) { + await this.tryToTransferWallet(input.anonymousUserId, user.id); + } + + return user; + } + + private async tryToRegisterUser(input: UserInitInput): Promise { + const user = await this.userRepository.create({ + userId: input.userId, + username: await this.generateUsername(input.wantedUsername), + email: input.email, + emailVerified: input.emailVerified, + subscribedToNewsletter: input.subscribedToNewsletter + }); + + this.logger.info({ event: "USER_REGISTERED", userId: input.userId, id: user.id }); + + return user; + } + + private async tryToUpdateUser(input: UserInitInput, user?: UserOutput) { + if (!user) { + return; + } + + if (user.email !== input.email || user.emailVerified !== input.emailVerified) { + const updatePayload = { + email: input.email, + emailVerified: input.emailVerified + }; + await this.userRepository.updateBy({ userId: input.userId }, updatePayload); + + return { + ...user, + ...updatePayload + }; + } + + return user; + } + + private async tryToTransferWallet(prevUserId: string, nextUserId: string) { + if (process.env.BILLING_ENABLED !== "true") { + return; + } + + try { + await this.userWalletRepository.updateBy({ userId: prevUserId }, { userId: nextUserId }); + } catch (error) { + if (!error.message.includes("user_wallets_user_id_unique")) { + throw error; + } + } + } + + private async generateUsername(wantedUsername: string, dbTransaction?: Transaction): Promise { + let baseUsername = wantedUsername.replace(/[^a-zA-Z0-9_-]/gi, ""); + + if (baseUsername.length < 3) { + baseUsername = "anonymous"; + } else if (baseUsername.length > 40) { + baseUsername = baseUsername.substring(0, 40); + } + + let username = baseUsername; + + while (!(await checkUsernameAvailable(username, dbTransaction))) { + username = baseUsername + this.randomIntFromInterval(1000, 9999); + } + + return username; + } + + private randomIntFromInterval(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); + } +} diff --git a/apps/api/test/functional/user-init.spec.ts b/apps/api/test/functional/user-init.spec.ts index 070165d53..cdc2b4f93 100644 --- a/apps/api/test/functional/user-init.spec.ts +++ b/apps/api/test/functional/user-init.spec.ts @@ -6,6 +6,7 @@ import omit from "lodash/omit"; import { container } from "tsyringe"; import { app } from "@src/app"; +import { AuthInterceptor } from "@src/auth/services/auth.interceptor"; import { USER_WALLET_SCHEMA, UserWalletSchema } from "@src/billing/providers"; import { UserWalletRepository } from "@src/billing/repositories"; import { ApiPgDatabase, POSTGRES_DB } from "@src/core"; @@ -26,6 +27,9 @@ describe("User Init", () => { const userWalletRepository = container.resolve(UserWalletRepository); const db = container.resolve(POSTGRES_DB); const walletService = new WalletService(app); + const authInterceptor = container.resolve(AuthInterceptor); + // @ts-ignore + const getValidUserId = jest.spyOn(authInterceptor, "getValidUserId"); let auth0Payload: { userId: string; wantedUsername: string; @@ -58,6 +62,7 @@ describe("User Init", () => { }; (getCurrentUserId as jest.Mock).mockReturnValue(auth0Payload.userId); + (getValidUserId as jest.Mock).mockResolvedValue(auth0Payload.userId); }); afterEach(async () => { @@ -118,20 +123,22 @@ describe("User Init", () => { }); async function sendTokenInfo(token?: string) { - const headers = new Headers({ "Content-Type": "application/json" }); + const headers = new Headers({ "Content-Type": "application/json", authorization: `Bearer some-auth0-token` }); if (token) { headers.set("x-anonymous-authorization", `Bearer ${token}`); } - const res = await app.request("/user/tokenInfo", { + const res = await app.request("/v1/users", { method: "POST", headers, - body: JSON.stringify(auth0Payload) + body: JSON.stringify({ data: auth0Payload }) }); + const body = await res.json(); + return { - body: await res.json(), + body: body.data, status: res.status }; } diff --git a/apps/deploy-web/.env-staging b/apps/deploy-web/.env-staging new file mode 100644 index 000000000..7b93c2fe0 --- /dev/null +++ b/apps/deploy-web/.env-staging @@ -0,0 +1,5 @@ +NEXT_PUBLIC_BILLING_ENABLED=true +NEXT_PUBLIC_API_MAINNET_BASE_URL=https://api-mainnet-staging.cloudmos.io +NEXT_PUBLIC_API_SANDBOX_BASE_URL=https://api-sandbox-staging.cloudmos.io +NEXT_PUBLIC_API_BASE_URL=https://api-mainnet-staging.cloudmos.io +NEXT_PUBLIC_MASTER_WALLET_ADDRESS=akash1ss0d2yw38r6e7ew8ndye9h7kg62sem36zak4d5 \ No newline at end of file