Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(user): implement anonymous user registration #282

Merged
merged 2 commits into from
Jul 30, 2024
Merged
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
9 changes: 5 additions & 4 deletions .env.sandbox.docker-compose-dev
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@opentelemetry/sdk-node": "^0.52.1",
"@sentry/node": "^7.55.2",
"@supercharge/promise-pool": "^3.2.0",
"axios": "^0.27.2",
"axios": "^1.7.2",
"commander": "^12.1.0",
"cosmjs-types": "^0.9.0",
"date-fns": "^2.29.2",
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ManagedUserWalletService, WalletInitializerService } from "@src/billing
import { TxSignerService } from "@src/billing/services/tx-signer/tx-signer.service";
import { WithTransaction } from "@src/core/services";

// TODO: authorize endpoints below
@singleton()
export class WalletController {
constructor(
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/core/providers/request.provider.ts
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<string, Context>>();

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<void>) {
return await new Promise((resolve, reject) => {
this.storage.run(new Map(), () => {
this.storage.getStore().set(this.CONTEXT_KEY, context);
cb().then(resolve).catch(reject);
});
});
}
}
13 changes: 10 additions & 3 deletions apps/api/src/db/dbConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -25,10 +26,14 @@ if (!csMap[env.Network]) {
throw new Error(`Missing connection string for network: ${env.Network}`);
}

const logger = new PostgresLoggerService({ orm: "sequelize" });
const logging = (msg: string) => logger.write(msg);

pg.defaults.parseInt8 = true;
export const chainDb = new Sequelize(csMap[env.Network], {
dialectModule: pg,
logging: false,
logging,
logQueryParameters: true,
transactionType: DbTransaction.TYPES.IMMEDIATE,
define: {
timestamps: false,
Expand All @@ -44,7 +49,8 @@ export const chainDbs: { [key: string]: Sequelize } = Object.keys(chainDefinitio
...obj,
[chain]: new Sequelize(chainDefinitions[chain].connectionString, {
dialectModule: pg,
logging: false,
logging,
logQueryParameters: true,
repositoryMode: true,
transactionType: DbTransaction.TYPES.IMMEDIATE,
define: {
Expand All @@ -59,7 +65,8 @@ export const chainDbs: { [key: string]: Sequelize } = Object.keys(chainDefinitio

export const userDb = new Sequelize(env.UserDatabaseCS, {
dialectModule: pg,
logging: false,
logging,
logQueryParameters: true,
transactionType: DbTransaction.TYPES.IMMEDIATE,
define: {
timestamps: false,
Expand Down
10 changes: 9 additions & 1 deletion apps/api/src/routers/userRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
96 changes: 69 additions & 27 deletions apps/api/src/services/db/userDataService.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -13,9 +16,7 @@ export async function checkUsernameAvailable(username: string, dbTransaction?: T
}

async function generateUsername(wantedUsername: string, dbTransaction?: Transaction): Promise<string> {
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";
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/user/providers/current-user.provider.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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();
};
}
}
3 changes: 2 additions & 1 deletion apps/deploy-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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",
Expand Down Expand Up @@ -51,7 +52,7 @@
"@tanstack/react-table": "^8.13.2",
"@textea/json-viewer": "^3.0.0",
"auth0": "^4.3.1",
"axios": "^0.27.2",
"axios": "^1.7.2",
"chain-registry": "^1.20.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
Expand Down
3 changes: 2 additions & 1 deletion apps/deploy-web/src/components/user/UserProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
<UserProvider>
<UserProvider fetcher={authHttpService.getProfile}>
<UserInitLoader>
<AnonymousUserProvider>{children}</AnonymousUserProvider>
</UserInitLoader>
Expand Down
Loading
Loading