hash clientSecret and better logging for sdk
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
packages/sdk/profileId.txt
|
||||
packages/sdk/test.ts
|
||||
|
||||
# Logs
|
||||
|
||||
25
README.md
25
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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
7
apps/backend/src/services/hash.ts
Normal file
7
apps/backend/src/services/hash.ts
Normal 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);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user