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
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
packages/sdk/profileId.txt
|
||||||
packages/sdk/test.ts
|
packages/sdk/test.ts
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -18,10 +18,29 @@ For pushing events
|
|||||||
import { Mixan } from '@mixan/sdk';
|
import { Mixan } from '@mixan/sdk';
|
||||||
|
|
||||||
const mixan = new Mixan({
|
const mixan = new Mixan({
|
||||||
clientSecret: '9fb405d2-7e16-489f-980c-67b25a6eab97',
|
clientId: 'uuid',
|
||||||
url: 'http://localhost:8080',
|
clientSecret: 'uuid',
|
||||||
|
url: 'http://localhost:8080/api/sdk',
|
||||||
batchInterval: 10000,
|
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({
|
mixan.setUser({
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ app.use(morgan(':method :url :status :response-time ms'))
|
|||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
app.get('/', (req, res) => res.json('Welcome to Mixan'))
|
app.get('/', (req, res) => res.json('Welcome to Mixan'))
|
||||||
if (process.env.SETUP) {
|
app.use('/setup', setup)
|
||||||
app.use('/setup', setup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
app.use(authMiddleware)
|
app.use(authMiddleware)
|
||||||
|
|||||||
@@ -1,27 +1,45 @@
|
|||||||
import { NextFunction, Request, Response } from "express"
|
import { NextFunction, Request, Response } from 'express'
|
||||||
import { db } from "../db"
|
import { db } from '../db'
|
||||||
import { createError } from "../responses/errors"
|
import { HttpError, createError } from '../responses/errors'
|
||||||
|
import { verifyPassword } from '../services/hash'
|
||||||
|
|
||||||
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
|
export async function authMiddleware(
|
||||||
const secret = req.headers['mixan-client-secret'] as string | undefined
|
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
|
||||||
|
|
||||||
if(!secret) {
|
if (!clientId) {
|
||||||
return next(createError(401, 'Misisng client secret'))
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await db.client.findFirst({
|
|
||||||
where: {
|
|
||||||
secret,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if(!client) {
|
|
||||||
return next(createError(401, 'Invalid client secret'))
|
|
||||||
}
|
|
||||||
|
|
||||||
req.client = {
|
|
||||||
project_id: client.project_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
import { NextFunction, Request, Response } from 'express'
|
import { NextFunction, Request, Response } from 'express'
|
||||||
import { db } from '../db'
|
import { db } from '../db'
|
||||||
import { v4 as uuid } from 'uuid'
|
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) {
|
export async function setup(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
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({
|
const organization = await db.organization.create({
|
||||||
data: {
|
data: {
|
||||||
name: 'Acme Inc.',
|
name: 'Acme Inc.',
|
||||||
@@ -16,20 +28,21 @@ export async function setup(req: Request, res: Response, next: NextFunction) {
|
|||||||
organization_id: organization.id,
|
organization_id: organization.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
const secret = uuid()
|
||||||
const client = await db.client.create({
|
const client = await db.client.create({
|
||||||
data: {
|
data: {
|
||||||
name: 'Acme Website Client',
|
name: 'Acme Website Client',
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
secret: '4bfc4a0b-37e0-4916-b634-95c6a32a2e77',
|
secret: await hashPassword(secret),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json({
|
res.json(
|
||||||
organization,
|
success({
|
||||||
project,
|
clientId: client.id,
|
||||||
client,
|
clientSecret: secret,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(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 = {
|
type MixanOptions = {
|
||||||
url: string
|
url: string
|
||||||
|
clientId: string
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
batchInterval?: number
|
batchInterval?: number
|
||||||
maxBatchSize?: number
|
maxBatchSize?: number
|
||||||
@@ -19,11 +20,13 @@ type MixanOptions = {
|
|||||||
|
|
||||||
class Fetcher {
|
class Fetcher {
|
||||||
private url: string
|
private url: string
|
||||||
|
private clientId: string
|
||||||
private clientSecret: string
|
private clientSecret: string
|
||||||
private logger: (...args: any[]) => void
|
private logger: (...args: any[]) => void
|
||||||
|
|
||||||
constructor(options: MixanOptions) {
|
constructor(options: MixanOptions) {
|
||||||
this.url = options.url
|
this.url = options.url
|
||||||
|
this.clientId = options.clientId
|
||||||
this.clientSecret = options.clientSecret
|
this.clientSecret = options.clientSecret
|
||||||
this.logger = options.verbose ? console.log : () => {}
|
this.logger = options.verbose ? console.log : () => {}
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,7 @@ class Fetcher {
|
|||||||
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2))
|
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2))
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
|
['mixan-client-id']: this.clientId,
|
||||||
['mixan-client-secret']: this.clientSecret,
|
['mixan-client-secret']: this.clientSecret,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
@@ -137,6 +141,7 @@ export class Mixan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event(name: string, properties: Record<string, any>) {
|
event(name: string, properties: Record<string, any>) {
|
||||||
|
this.logger('Mixan: Queue event', name)
|
||||||
this.eventBatcher.add({
|
this.eventBatcher.add({
|
||||||
name,
|
name,
|
||||||
properties,
|
properties,
|
||||||
@@ -149,10 +154,10 @@ export class Mixan {
|
|||||||
const profileId = this.options.getProfileId()
|
const profileId = this.options.getProfileId()
|
||||||
if(profileId) {
|
if(profileId) {
|
||||||
this.profileId = profileId
|
this.profileId = profileId
|
||||||
this.logger('Use existing ID', this.profileId);
|
this.logger('Mixan: Use existing ID', this.profileId);
|
||||||
} else {
|
} else {
|
||||||
this.profileId = uuid()
|
this.profileId = uuid()
|
||||||
this.logger('Create new ID', this.profileId);
|
this.logger('Mixan: Create new ID', this.profileId);
|
||||||
this.options.saveProfileId(this.profileId)
|
this.options.saveProfileId(this.profileId)
|
||||||
this.fetch.post('/profiles', {
|
this.fetch.post('/profiles', {
|
||||||
id: this.profileId,
|
id: this.profileId,
|
||||||
@@ -162,12 +167,20 @@ export class Mixan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUser(profile: ProfilePayload) {
|
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, {
|
await this.fetch.post(`/profiles/${this.profileId}`, profile, {
|
||||||
method: 'PUT'
|
method: 'PUT'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUserProperty(name: string, value: any) {
|
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}`, {
|
await this.fetch.post(`/profiles/${this.profileId}`, {
|
||||||
properties: {
|
properties: {
|
||||||
[name]: value,
|
[name]: value,
|
||||||
@@ -177,9 +190,11 @@ export class Mixan {
|
|||||||
|
|
||||||
async increment(name: string, value: number = 1) {
|
async increment(name: string, value: number = 1) {
|
||||||
if (!this.profileId) {
|
if (!this.profileId) {
|
||||||
|
this.logger('Mixan: Increment failed, no profileId');
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger('Mixan: Increment user property', name, value);
|
||||||
await this.fetch.post(`/profiles/${this.profileId}/increment`, {
|
await this.fetch.post(`/profiles/${this.profileId}/increment`, {
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
@@ -190,9 +205,11 @@ export class Mixan {
|
|||||||
|
|
||||||
async decrement(name: string, value: number = 1) {
|
async decrement(name: string, value: number = 1) {
|
||||||
if (!this.profileId) {
|
if (!this.profileId) {
|
||||||
|
this.logger('Mixan: Decrement failed, no profileId');
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger('Mixan: Decrement user property', name, value);
|
||||||
await this.fetch.post(`/profiles/${this.profileId}/decrement`, {
|
await this.fetch.post(`/profiles/${this.profileId}/decrement`, {
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
@@ -209,8 +226,10 @@ export class Mixan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.eventBatcher.flush()
|
this.logger('Mixan: Clear, send remaining events and remove profileId');
|
||||||
|
this.eventBatcher.send()
|
||||||
this.options.removeProfileId()
|
this.options.removeProfileId()
|
||||||
this.profileId = undefined
|
this.profileId = undefined
|
||||||
|
this.setAnonymousUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user