diff --git a/.gitignore b/.gitignore index 36e5d14a..2e015d38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore +packages/sdk/profileId.txt packages/sdk/test.ts # Logs diff --git a/README.md b/README.md index cadc9d2b..a919bb31 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,29 @@ For pushing events import { Mixan } from '@mixan/sdk'; const mixan = new Mixan({ - clientSecret: '9fb405d2-7e16-489f-980c-67b25a6eab97', - url: 'http://localhost:8080', + clientId: 'uuid', + clientSecret: 'uuid', + url: 'http://localhost:8080/api/sdk', batchInterval: 10000, - verbose: false + verbose: false, + saveProfileId(id) { + // Web + localStorage.setItem('@profileId', id) + // // react-native-mmkv + // mmkv.setItem('@profileId', id) + }, + removeProfileId() { + // Web + localStorage.removeItem('@profileId') + // // react-native-mmkv + // mmkv.delete('@profileId') + }, + getProfileId() { + // Web + return localStorage.getItem('@profileId') + // // react-native-mmkv + // return mmkv.getString('@profileId') + }, }) mixan.setUser({ diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 88c27d64..53676e55 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -14,9 +14,7 @@ app.use(morgan(':method :url :status :response-time ms')) // Public routes app.get('/', (req, res) => res.json('Welcome to Mixan')) -if (process.env.SETUP) { - app.use('/setup', setup) -} +app.use('/setup', setup) // Protected routes app.use(authMiddleware) diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts index 1bf182d2..7ae7ed24 100644 --- a/apps/backend/src/middlewares/auth.ts +++ b/apps/backend/src/middlewares/auth.ts @@ -1,27 +1,45 @@ -import { NextFunction, Request, Response } from "express" -import { db } from "../db" -import { createError } from "../responses/errors" +import { NextFunction, Request, Response } from 'express' +import { db } from '../db' +import { HttpError, createError } from '../responses/errors' +import { verifyPassword } from '../services/hash' -export async function authMiddleware(req: Request, res: Response, next: NextFunction) { - const secret = req.headers['mixan-client-secret'] as string | undefined - - if(!secret) { - return next(createError(401, 'Misisng client secret')) - } - - const client = await db.client.findFirst({ - where: { - secret, - }, - }) - - if(!client) { - return next(createError(401, 'Invalid client secret')) - } +export async function authMiddleware( + req: Request, + res: Response, + next: NextFunction +) { + try { + const clientId = req.headers['mixan-client-id'] as string | undefined + const clientSecret = req.headers['mixan-client-secret'] as string | undefined - req.client = { - project_id: client.project_id, - } + if (!clientId) { + return next(createError(401, 'Misisng client id')) + } + + if (!clientSecret) { + return next(createError(401, 'Misisng client secret')) + } + + const client = await db.client.findUnique({ + where: { + id: clientId, + }, + }) - next() -} \ No newline at end of file + if(!client) { + return next(createError(401, 'Invalid client id')) + } + + if (!await verifyPassword(clientSecret, client.secret)) { + return next(createError(401, 'Invalid client secret')) + } + + req.client = { + project_id: client.project_id, + } + + next() + } catch (error) { + next(new HttpError(500, 'Failed verify client credentials')) + } +} diff --git a/apps/backend/src/routes/setup.ts b/apps/backend/src/routes/setup.ts index 8e9527bb..e803b642 100644 --- a/apps/backend/src/routes/setup.ts +++ b/apps/backend/src/routes/setup.ts @@ -1,9 +1,21 @@ import { NextFunction, Request, Response } from 'express' import { db } from '../db' import { v4 as uuid } from 'uuid' +import { hashPassword } from '../services/hash' +import { success } from '../responses/success' export async function setup(req: Request, res: Response, next: NextFunction) { try { + const counts = await db.$transaction([ + db.organization.count(), + db.project.count(), + db.client.count(), + ]) + + if (counts.some((count) => count > 0)) { + return res.json(success('Setup already done')) + } + const organization = await db.organization.create({ data: { name: 'Acme Inc.', @@ -16,20 +28,21 @@ export async function setup(req: Request, res: Response, next: NextFunction) { organization_id: organization.id, }, }) - + const secret = uuid() const client = await db.client.create({ data: { name: 'Acme Website Client', project_id: project.id, - secret: '4bfc4a0b-37e0-4916-b634-95c6a32a2e77', + secret: await hashPassword(secret), }, }) - res.json({ - organization, - project, - client, - }) + res.json( + success({ + clientId: client.id, + clientSecret: secret, + }) + ) } catch (error) { next(error) } diff --git a/apps/backend/src/services/hash.ts b/apps/backend/src/services/hash.ts new file mode 100644 index 00000000..0c38644d --- /dev/null +++ b/apps/backend/src/services/hash.ts @@ -0,0 +1,7 @@ +export async function hashPassword(password: string) { + return await Bun.password.hash(password); +} + +export async function verifyPassword(password: string, hashedPassword: string) { + return await Bun.password.verify(password, hashedPassword); +} diff --git a/packages/sdk/index.ts b/packages/sdk/index.ts index 86d9f714..18f22a2e 100644 --- a/packages/sdk/index.ts +++ b/packages/sdk/index.ts @@ -8,6 +8,7 @@ import { type MixanOptions = { url: string + clientId: string clientSecret: string batchInterval?: number maxBatchSize?: number @@ -19,11 +20,13 @@ type MixanOptions = { class Fetcher { private url: string + private clientId: string private clientSecret: string private logger: (...args: any[]) => void constructor(options: MixanOptions) { this.url = options.url + this.clientId = options.clientId this.clientSecret = options.clientSecret this.logger = options.verbose ? console.log : () => {} } @@ -37,6 +40,7 @@ class Fetcher { this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2)) return fetch(url, { headers: { + ['mixan-client-id']: this.clientId, ['mixan-client-secret']: this.clientSecret, 'Content-Type': 'application/json', }, @@ -137,6 +141,7 @@ export class Mixan { } event(name: string, properties: Record) { + this.logger('Mixan: Queue event', name) this.eventBatcher.add({ name, properties, @@ -149,10 +154,10 @@ export class Mixan { const profileId = this.options.getProfileId() if(profileId) { this.profileId = profileId - this.logger('Use existing ID', this.profileId); + this.logger('Mixan: Use existing ID', this.profileId); } else { this.profileId = uuid() - this.logger('Create new ID', this.profileId); + this.logger('Mixan: Create new ID', this.profileId); this.options.saveProfileId(this.profileId) this.fetch.post('/profiles', { id: this.profileId, @@ -162,12 +167,20 @@ export class Mixan { } async setUser(profile: ProfilePayload) { + if(!this.profileId) { + return this.logger('Mixan: Set user failed, no profileId'); + } + this.logger('Mixan: Set user', profile); await this.fetch.post(`/profiles/${this.profileId}`, profile, { method: 'PUT' }) } async setUserProperty(name: string, value: any) { + if(!this.profileId) { + return this.logger('Mixan: Set user property, no profileId'); + } + this.logger('Mixan: Set user property', name, value); await this.fetch.post(`/profiles/${this.profileId}`, { properties: { [name]: value, @@ -177,9 +190,11 @@ export class Mixan { async increment(name: string, value: number = 1) { if (!this.profileId) { + this.logger('Mixan: Increment failed, no profileId'); return } + this.logger('Mixan: Increment user property', name, value); await this.fetch.post(`/profiles/${this.profileId}/increment`, { name, value, @@ -190,9 +205,11 @@ export class Mixan { async decrement(name: string, value: number = 1) { if (!this.profileId) { + this.logger('Mixan: Decrement failed, no profileId'); return } + this.logger('Mixan: Decrement user property', name, value); await this.fetch.post(`/profiles/${this.profileId}/decrement`, { name, value, @@ -209,8 +226,10 @@ export class Mixan { } clear() { - this.eventBatcher.flush() + this.logger('Mixan: Clear, send remaining events and remove profileId'); + this.eventBatcher.send() this.options.removeProfileId() this.profileId = undefined + this.setAnonymousUser() } }