From b58d74a8ba0412f1ff8eeeaecafa1a2369723cbf Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Mon, 29 Jul 2024 14:11:14 +0200 Subject: [PATCH] feat(user): implement anonymous user registration refs #247 --- .env.sandbox.docker-compose-dev | 9 +- apps/api/src/app.ts | 4 + .../src/core/providers/request.provider.ts | 13 +++ .../postgres-logger.service.ts | 14 ++- .../request-storage.interceptor.ts | 35 +++++++ apps/api/src/db/dbConnection.ts | 9 +- apps/api/src/routers/userRouter.ts | 10 +- apps/api/src/services/db/userDataService.ts | 96 +++++++++++++------ .../user/providers/current-user.provider.ts | 16 ++++ .../current-user/current-user.interceptor.ts | 19 ++++ apps/deploy-web/package.json | 1 + .../src/components/user/UserProviders.tsx | 3 +- apps/deploy-web/src/hooks/useCustomUser.ts | 1 - .../src/hooks/useStoredAnonymousUser.ts | 9 +- .../src/pages/api/auth/[...auth0].ts | 20 ++-- apps/deploy-web/src/pages/api/auth/signup.ts | 3 +- .../src/services/auth/auth-http.service.ts | 27 ++++++ apps/deploy-web/src/utils/constants.ts | 2 + package-lock.json | 1 + packages/http-sdk/src/index.ts | 1 + 20 files changed, 243 insertions(+), 50 deletions(-) create mode 100644 apps/api/src/core/providers/request.provider.ts create mode 100644 apps/api/src/core/services/request-storage/request-storage.interceptor.ts create mode 100644 apps/api/src/user/providers/current-user.provider.ts create mode 100644 apps/api/src/user/services/current-user/current-user.interceptor.ts create mode 100644 apps/deploy-web/src/services/auth/auth-http.service.ts diff --git a/.env.sandbox.docker-compose-dev b/.env.sandbox.docker-compose-dev index 2fac03e61..0b87791a1 100644 --- a/.env.sandbox.docker-compose-dev +++ b/.env.sandbox.docker-compose-dev @@ -5,13 +5,14 @@ Network=sandbox ActiveChain=akashSandbox # Deploy Web -API_BASE_URL: http://api:3080 +API_BASE_URL: http://api:3000 +BASE_API_MAINNET_URL: http://api:3000 PROVIDER_PROXY_URL: http://provider-proxy:3040 # Stats Web -API_MAINNET_BASE_URL: http://api:3080 -API_TESTNET_BASE_URL: http://api:3080 -API_SANDBOX_BASE_URL: http://api:3080 +API_MAINNET_BASE_URL: http://api:3000 +API_TESTNET_BASE_URL: http://api:3000 +API_SANDBOX_BASE_URL: http://api:3000 # DB POSTGRES_USER: postgres diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index c31cc4ab1..c16e8f13a 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -13,6 +13,8 @@ import { container } from "tsyringe"; import { HonoErrorHandlerService } from "@src/core/services/hono-error-handler/hono-error-handler.service"; import { HttpLoggerService } from "@src/core/services/http-logger/http-logger.service"; import { LoggerService } from "@src/core/services/logger/logger.service"; +import { RequestStorageInterceptor } from "@src/core/services/request-storage/request-storage.interceptor"; +import { CurrentUserInterceptor } from "@src/user/services/current-user/current-user.interceptor"; import packageJson from "../package.json"; import { chainDb, syncUserSchema, userDb } from "./db/dbConnection"; import { apiRouter } from "./routers/apiRouter"; @@ -62,6 +64,8 @@ const scheduler = new Scheduler({ }); appHono.use(container.resolve(HttpLoggerService).intercept()); +appHono.use(container.resolve(RequestStorageInterceptor).intercept()); +appHono.use(container.resolve(CurrentUserInterceptor).intercept()); appHono.use( "*", sentry({ diff --git a/apps/api/src/core/providers/request.provider.ts b/apps/api/src/core/providers/request.provider.ts new file mode 100644 index 000000000..64e3a8b68 --- /dev/null +++ b/apps/api/src/core/providers/request.provider.ts @@ -0,0 +1,13 @@ +import { container, inject } from "tsyringe"; + +import { RequestStorageInterceptor } from "@src/core/services/request-storage/request-storage.interceptor"; + +const REQUEST = "REQUEST"; + +container.register(REQUEST, { + useFactory: c => { + return c.resolve(RequestStorageInterceptor).context; + } +}); + +export const Request = () => inject(REQUEST); diff --git a/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts b/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts index 86806b0f6..e1b0680b4 100644 --- a/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts +++ b/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts @@ -4,13 +4,21 @@ import { format } from "sql-formatter"; import { LoggerService } from "@src/core/services/logger/logger.service"; export class PostgresLoggerService implements LogWriter { - private readonly logger = new LoggerService({ context: "POSTGRES" }); + private readonly logger: LoggerService; + + private readonly isDrizzle: boolean; + + constructor(options?: { orm: "drizzle" | "sequelize" }) { + const orm = options?.orm || "drizzle"; + this.logger = new LoggerService({ context: "POSTGRES", orm }); + this.isDrizzle = orm === "drizzle"; + } write(message: string) { - let formatted = message.replace(/^Query: /, ""); + let formatted = message.replace(this.isDrizzle ? /^Query: / : /^Executing \(default\):/, ""); if (this.logger.isPretty) { - formatted = format(message, { language: "postgresql" }); + formatted = format(formatted, { language: "postgresql" }); } this.logger.debug(formatted); diff --git a/apps/api/src/core/services/request-storage/request-storage.interceptor.ts b/apps/api/src/core/services/request-storage/request-storage.interceptor.ts new file mode 100644 index 000000000..f812f97d4 --- /dev/null +++ b/apps/api/src/core/services/request-storage/request-storage.interceptor.ts @@ -0,0 +1,35 @@ +import { Context, Next } from "hono"; +import { AsyncLocalStorage } from "node:async_hooks"; +import { singleton } from "tsyringe"; +import { v4 as uuid } from "uuid"; + +import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type"; + +@singleton() +export class RequestStorageInterceptor implements HonoInterceptor { + private readonly CONTEXT_KEY = "CONTEXT"; + + private readonly storage = new AsyncLocalStorage>(); + + get context() { + return this.storage.getStore()?.get(this.CONTEXT_KEY); + } + + intercept() { + return async (c: Context, next: Next) => { + const requestId = c.req.header("X-Request-Id") || uuid(); + c.set("requestId", requestId); + + await this.runWithContext(c, next); + }; + } + + private async runWithContext(context: Context, cb: () => Promise) { + return await new Promise((resolve, reject) => { + this.storage.run(new Map(), () => { + this.storage.getStore().set(this.CONTEXT_KEY, context); + cb().then(resolve).catch(reject); + }); + }); + } +} diff --git a/apps/api/src/db/dbConnection.ts b/apps/api/src/db/dbConnection.ts index 92a7f3b2a..e6246361d 100644 --- a/apps/api/src/db/dbConnection.ts +++ b/apps/api/src/db/dbConnection.ts @@ -5,6 +5,7 @@ import pg from "pg"; import { Transaction as DbTransaction } from "sequelize"; import { Sequelize } from "sequelize-typescript"; +import { PostgresLoggerService } from "@src/core/services/postgres-logger/postgres-logger.service"; import { env } from "@src/utils/env"; function isValidNetwork(network: string): network is keyof typeof csMap { @@ -25,10 +26,12 @@ if (!csMap[env.Network]) { throw new Error(`Missing connection string for network: ${env.Network}`); } +const logger = new PostgresLoggerService({ orm: "sequelize" }); + pg.defaults.parseInt8 = true; export const chainDb = new Sequelize(csMap[env.Network], { dialectModule: pg, - logging: false, + logging: (msg: string) => logger.write(msg), transactionType: DbTransaction.TYPES.IMMEDIATE, define: { timestamps: false, @@ -44,7 +47,7 @@ export const chainDbs: { [key: string]: Sequelize } = Object.keys(chainDefinitio ...obj, [chain]: new Sequelize(chainDefinitions[chain].connectionString, { dialectModule: pg, - logging: false, + logging: (msg: string) => logger.write(msg), repositoryMode: true, transactionType: DbTransaction.TYPES.IMMEDIATE, define: { @@ -59,7 +62,7 @@ export const chainDbs: { [key: string]: Sequelize } = Object.keys(chainDefinitio export const userDb = new Sequelize(env.UserDatabaseCS, { dialectModule: pg, - logging: false, + logging: (msg: string) => logger.write(msg), transactionType: DbTransaction.TYPES.IMMEDIATE, define: { timestamps: false, diff --git a/apps/api/src/routers/userRouter.ts b/apps/api/src/routers/userRouter.ts index b4c4de3e2..3c043d7cd 100644 --- a/apps/api/src/routers/userRouter.ts +++ b/apps/api/src/routers/userRouter.ts @@ -114,9 +114,17 @@ userRequiredRouter.delete("/removeAddressName/:address", async c => { userRequiredRouter.post("/tokenInfo", async c => { const userId = getCurrentUserId(c); + const anonymousUserId = c.req.header("x-anonymous-user-id"); const { wantedUsername, email, emailVerified, subscribedToNewsletter } = await c.req.json(); - const settings = await getSettingsOrInit(userId, wantedUsername, email, !!emailVerified, subscribedToNewsletter); + const settings = await getSettingsOrInit({ + anonymousUserId, + userId: userId, + wantedUsername, + email: email, + emailVerified: !!emailVerified, + subscribedToNewsletter: subscribedToNewsletter + }); return c.json(settings); }); diff --git a/apps/api/src/services/db/userDataService.ts b/apps/api/src/services/db/userDataService.ts index acf56d359..1b46faa13 100644 --- a/apps/api/src/services/db/userDataService.ts +++ b/apps/api/src/services/db/userDataService.ts @@ -1,7 +1,10 @@ import { UserAddressName, UserSetting } from "@akashnetwork/database/dbSchemas/user"; +import pick from "lodash/pick"; import { Transaction } from "sequelize"; -import { getUserPlan } from "../external/stripeService"; +import { LoggerService } from "@src/core"; + +const logger = new LoggerService({ context: "UserDataService" }); function randomIntFromInterval(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); @@ -13,9 +16,7 @@ export async function checkUsernameAvailable(username: string, dbTransaction?: T } async function generateUsername(wantedUsername: string, dbTransaction?: Transaction): Promise { - const sanitized = wantedUsername.replace(/[^a-zA-Z0-9_-]/gi, ""); - - let baseUsername = sanitized; + let baseUsername = wantedUsername.replace(/[^a-zA-Z0-9_-]/gi, ""); if (baseUsername.length < 3) { baseUsername = "anonymous"; @@ -61,41 +62,82 @@ export async function updateSettings( await settings.save(); } -export async function getSettingsOrInit(userId: string, wantedUsername: string, email: string, emailVerified: boolean, subscribedToNewsletter: boolean) { - const [userSettings, created] = await UserSetting.findCreateFind({ - where: { userId: userId }, - defaults: { +type UserInput = { + anonymousUserId?: string; + userId: string; + wantedUsername: string; + email: string; + emailVerified: boolean; + subscribedToNewsletter: boolean; +}; + +export async function getSettingsOrInit({ anonymousUserId, userId, wantedUsername, email, emailVerified, subscribedToNewsletter }: UserInput) { + let userSettings: UserSetting; + let isAnonymous = false; + + if (anonymousUserId) { + try { + const updateResult = await UserSetting.update( + { + userId, + username: await generateUsername(wantedUsername), + email: email, + emailVerified: emailVerified, + stripeCustomerId: null, + subscribedToNewsletter: subscribedToNewsletter + }, + { where: { id: anonymousUserId, userId: null }, returning: ["*"] } + ); + + userSettings = updateResult[1][0]; + isAnonymous = !!userSettings; + + if (isAnonymous) { + logger.info({ event: "ANONYMOUS_USER_REGISTERED", id: anonymousUserId, userId }); + } + } catch (error) { + if (error.name !== "SequelizeUniqueConstraintError") { + throw error; + } + + logger.info({ event: "ANONYMOUS_USER_ALREADY_REGISTERED", id: anonymousUserId, userId }); + } + } + + if (!isAnonymous) { + userSettings = await UserSetting.findOne({ where: { userId: userId } }); + logger.debug({ event: "USER_RETRIEVED", id: anonymousUserId, userId }); + } + + if (!userSettings) { + userSettings = await UserSetting.create({ userId: userId, username: await generateUsername(wantedUsername), email: email, emailVerified: emailVerified, stripeCustomerId: null, subscribedToNewsletter: subscribedToNewsletter - } - }); + }); + logger.info({ event: "USER_REGISTERED", userId }); + } - if (created) { - console.log(`Created settings for user ${userId}`); - } else if (userSettings.email !== email || userSettings.emailVerified !== emailVerified) { + if (userSettings.email !== email || userSettings.emailVerified !== emailVerified) { userSettings.email = email; userSettings.emailVerified = emailVerified; await userSettings.save(); } - const planCode = await getUserPlan(userSettings.stripeCustomerId); - - return { - username: userSettings.username, - email: userSettings.email, - emailVerified: userSettings.emailVerified, - stripeCustomerId: userSettings.stripeCustomerId, - bio: userSettings.bio, - subscribedToNewsletter: userSettings.subscribedToNewsletter, - youtubeUsername: userSettings.youtubeUsername, - twitterUsername: userSettings.twitterUsername, - githubUsername: userSettings.githubUsername, - planCode: planCode - }; + return pick(userSettings, [ + "username", + "email", + "emailVerified", + "stripeCustomerId", + "bio", + "subscribedToNewsletter", + "youtubeUsername", + "twitterUsername", + "githubUsername" + ]); } export async function getAddressNames(userId: string) { diff --git a/apps/api/src/user/providers/current-user.provider.ts b/apps/api/src/user/providers/current-user.provider.ts new file mode 100644 index 000000000..c593cdd31 --- /dev/null +++ b/apps/api/src/user/providers/current-user.provider.ts @@ -0,0 +1,16 @@ +import { container, inject } from "tsyringe"; + +import { RequestStorageInterceptor } from "@src/core/services/request-storage/request-storage.interceptor"; +import { CURRENT_USER } from "@src/user/services/current-user/current-user.interceptor"; + +container.register(CURRENT_USER, { + useFactory: c => { + return c.resolve(RequestStorageInterceptor).context.get(CURRENT_USER); + } +}); + +export const CurrentUser = () => inject(CURRENT_USER); +export type CurrentUser = { + userId: string; + isAnonymous: boolean; +}; diff --git a/apps/api/src/user/services/current-user/current-user.interceptor.ts b/apps/api/src/user/services/current-user/current-user.interceptor.ts new file mode 100644 index 000000000..f2eed5515 --- /dev/null +++ b/apps/api/src/user/services/current-user/current-user.interceptor.ts @@ -0,0 +1,19 @@ +import { Context, Next } from "hono"; +import { singleton } from "tsyringe"; + +import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type"; +import { getCurrentUserId } from "@src/middlewares/userMiddleware"; + +export const CURRENT_USER = "CURRENT_USER"; + +@singleton() +export class CurrentUserInterceptor implements HonoInterceptor { + intercept() { + return async (c: Context, next: Next) => { + const userId = getCurrentUserId(c); + c.set(CURRENT_USER, { userId, isAnonymous: !userId }); + + return await next(); + }; + } +} diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index dc3558d27..d21a06afc 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -18,6 +18,7 @@ "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.10.0", "@akashnetwork/ui": "*", + "@akashnetwork/http-sdk": "*", "@auth0/nextjs-auth0": "^3.5.0", "@chain-registry/types": "^0.41.3", "@cosmjs/encoding": "^0.32.4", diff --git a/apps/deploy-web/src/components/user/UserProviders.tsx b/apps/deploy-web/src/components/user/UserProviders.tsx index 7698c8a1e..45edf4a37 100644 --- a/apps/deploy-web/src/components/user/UserProviders.tsx +++ b/apps/deploy-web/src/components/user/UserProviders.tsx @@ -3,11 +3,12 @@ import { UserProvider } from "@auth0/nextjs-auth0/client"; import { UserInitLoader } from "@src/components/user/UserInitLoader"; import { envConfig } from "@src/config/env.config"; import { AnonymousUserProvider } from "@src/context/AnonymousUserProvider/AnonymousUserProvider"; +import { authHttpService } from "@src/services/auth/auth-http.service"; import { FCWithChildren } from "@src/types/component"; export const UserProviders: FCWithChildren = ({ children }) => envConfig.NEXT_PUBLIC_BILLING_ENABLED ? ( - + {children} diff --git a/apps/deploy-web/src/hooks/useCustomUser.ts b/apps/deploy-web/src/hooks/useCustomUser.ts index d7653b8c0..302fdea97 100644 --- a/apps/deploy-web/src/hooks/useCustomUser.ts +++ b/apps/deploy-web/src/hooks/useCustomUser.ts @@ -12,7 +12,6 @@ type UseCustomUser = { export const useCustomUser = (): UseCustomUser => { const { user, isLoading, error, checkSession } = useUser(); - const completeUser = user ? { ...user, plan: plans.find(x => x.code === user.planCode) } : user; return { diff --git a/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts b/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts index 6fd55c965..9179ffb73 100644 --- a/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts +++ b/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts @@ -1,24 +1,27 @@ import { useMemo } from "react"; -import { useLocalStorage } from "usehooks-ts"; import { envConfig } from "@src/config/env.config"; import { useCustomUser } from "@src/hooks/useCustomUser"; import { useWhen } from "@src/hooks/useWhen"; import { useAnonymousUserQuery, UserOutput } from "@src/queries/useAnonymousUserQuery"; +import { ANONYMOUS_USER_KEY } from "@src/utils/constants"; type UseApiUserResult = { user?: UserOutput; isLoading: boolean; }; +const storedAnonymousUserStr = typeof window !== "undefined" && localStorage.getItem(ANONYMOUS_USER_KEY); +const storedAnonymousUser: UserOutput | undefined = storedAnonymousUserStr ? JSON.parse(storedAnonymousUserStr) : undefined; + export const useStoredAnonymousUser = (): UseApiUserResult => { const { user: registeredUser, isLoading: isLoadingRegisteredUser } = useCustomUser(); - const [storedAnonymousUser, storeAnonymousUser] = useLocalStorage("user", undefined); const { user, isLoading } = useAnonymousUserQuery(storedAnonymousUser?.id, { enabled: envConfig.NEXT_PUBLIC_BILLING_ENABLED && !registeredUser && !isLoadingRegisteredUser }); - useWhen(user, () => storeAnonymousUser(user)); + useWhen(user, () => localStorage.setItem("anonymous-user", JSON.stringify(user))); + useWhen(storedAnonymousUser && registeredUser, () => localStorage.removeItem(ANONYMOUS_USER_KEY)); return useMemo( () => ({ diff --git a/apps/deploy-web/src/pages/api/auth/[...auth0].ts b/apps/deploy-web/src/pages/api/auth/[...auth0].ts index d553b9c18..bf9995e5f 100644 --- a/apps/deploy-web/src/pages/api/auth/[...auth0].ts +++ b/apps/deploy-web/src/pages/api/auth/[...auth0].ts @@ -1,18 +1,19 @@ // pages/api/auth/[...auth0].js import { handleAuth, handleLogin, handleProfile } from "@auth0/nextjs-auth0"; -import axios from "axios"; +import axios, { AxiosRequestHeaders } from "axios"; +import type { NextApiRequest, NextApiResponse } from "next"; import { BASE_API_MAINNET_URL } from "@src/utils/constants"; export default handleAuth({ - async login(req, res) { + async login(req: NextApiRequest, res: NextApiResponse) { const returnUrl = decodeURIComponent((req.query.from as string) ?? "/"); await handleLogin(req, res, { returnTo: returnUrl }); }, - async profile(req, res) { + async profile(req: NextApiRequest, res: NextApiResponse) { console.log("server /profile", req.url); try { await handleProfile(req, res, { @@ -21,6 +22,15 @@ export default handleAuth({ try { // TODO: Fix for console const user_metadata = session.user["https://console.akash.network/user_metadata"]; + const headers: AxiosRequestHeaders = { + Authorization: `Bearer ${session.accessToken}` + }; + + const anonymousId = req.headers["x-anonymous-user-id"]; + + if (anonymousId) { + headers["X-ANONYMOUS-USER-ID"] = anonymousId as string; + } const userSettings = await axios.post( `${BASE_API_MAINNET_URL}/user/tokenInfo`, @@ -31,9 +41,7 @@ export default handleAuth({ subscribedToNewsletter: user_metadata?.subscribedToNewsletter === "true" }, { - headers: { - Authorization: `Bearer ${session.accessToken}` - } + headers } ); diff --git a/apps/deploy-web/src/pages/api/auth/signup.ts b/apps/deploy-web/src/pages/api/auth/signup.ts index c49f455cc..846fec196 100644 --- a/apps/deploy-web/src/pages/api/auth/signup.ts +++ b/apps/deploy-web/src/pages/api/auth/signup.ts @@ -1,6 +1,7 @@ import { handleLogin } from "@auth0/nextjs-auth0"; +import type { NextApiRequest, NextApiResponse } from "next"; -export default async function signup(req, res) { +export default async function signup(req: NextApiRequest, res: NextApiResponse) { try { await handleLogin(req, res, { authorizationParams: { diff --git a/apps/deploy-web/src/services/auth/auth-http.service.ts b/apps/deploy-web/src/services/auth/auth-http.service.ts new file mode 100644 index 000000000..da7a0346d --- /dev/null +++ b/apps/deploy-web/src/services/auth/auth-http.service.ts @@ -0,0 +1,27 @@ +import { HttpService } from "@akashnetwork/http-sdk"; +import type { UserProfile } from "@auth0/nextjs-auth0/client"; +import { AxiosRequestHeaders } from "axios"; + +import { ANONYMOUS_USER_KEY } from "@src/utils/constants"; + +export class AuthHttpService extends HttpService { + constructor() { + super(); + this.getProfile = this.getProfile.bind(this); + } + + async getProfile(url: string) { + try { + const user = localStorage.getItem(ANONYMOUS_USER_KEY); + const anonymousUserId = user ? JSON.parse(user).id : undefined; + const headers: AxiosRequestHeaders = anonymousUserId ? { "X-ANONYMOUS-USER-ID": anonymousUserId } : {}; + + return this.extractData(await this.get(url, { headers })); + } catch (error) { + console.warn("DEBUG error", error); + throw error; + } + } +} + +export const authHttpService = new AuthHttpService(); diff --git a/apps/deploy-web/src/utils/constants.ts b/apps/deploy-web/src/utils/constants.ts index 2b1953c6b..31da68b3f 100644 --- a/apps/deploy-web/src/utils/constants.ts +++ b/apps/deploy-web/src/utils/constants.ts @@ -179,3 +179,5 @@ export const monacoOptions = { }; export const txFeeBuffer = 10000; // 10000 uAKT + +export const ANONYMOUS_USER_KEY = "anonymous-user"; diff --git a/package-lock.json b/package-lock.json index 532a083ca..b4e70dd9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -261,6 +261,7 @@ "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.10.0", + "@akashnetwork/http-sdk": "*", "@akashnetwork/ui": "*", "@auth0/nextjs-auth0": "^3.5.0", "@chain-registry/types": "^0.41.3", diff --git a/packages/http-sdk/src/index.ts b/packages/http-sdk/src/index.ts index da83a678e..c4f89d524 100644 --- a/packages/http-sdk/src/index.ts +++ b/packages/http-sdk/src/index.ts @@ -1 +1,2 @@ +export * from "./http/http.service"; export * from "./allowance/allowance-http.service";