hash clientSecret and better logging for sdk

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-12 13:51:11 +02:00
parent 888d6db16b
commit 5e382911a8
7 changed files with 115 additions and 40 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
packages/sdk/profileId.txt
packages/sdk/test.ts
# Logs

View File

@@ -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({

View File

@@ -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)

View File

@@ -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()
}
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'))
}
}

View File

@@ -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)
}

View File

@@ -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);
}

View File

@@ -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<string, any>) {
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()
}
}