This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-11 12:34:35 +02:00
commit 903fd155c3
40 changed files with 1385 additions and 0 deletions

41
apps/backend/src/app.ts Normal file
View File

@@ -0,0 +1,41 @@
import express from "express";
import events from './routes/events'
import profiles from './routes/profiles'
import { authMiddleware } from "./middlewares/auth";
import morgan from 'morgan'
import { db } from "./db";
import { hashPassword } from "./services/password";
import { v4 as uuid } from 'uuid';
const app = express();
const port = 8080;
app.use(morgan('tiny'))
app.use(express.json());
app.use(express.json());
app.use(authMiddleware)
app.use(events)
app.use(profiles)
app.get("/ping", (req, res) => res.json("pong"));
app.listen(port, () => {
console.log(`Listening on port ${port}...`);
});
// async function main() {
// const secret = uuid()
// await db.client.create({
// data: {
// project_id: 'eed345ae-2772-42e5-b989-e36e09c5febc',
// name: 'test',
// secret: await hashPassword(secret),
// }
// })
// console.log('Your secret is', secret);
// }
// main()

3
apps/backend/src/db.ts Normal file
View File

@@ -0,0 +1,3 @@
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();

View File

@@ -0,0 +1,33 @@
import { NextFunction, Request, Response } from "express"
import { db } from "../db"
import { verifyPassword } from "../services/password"
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
const secret = req.headers['mixan-client-secret'] as string | undefined
if(!secret) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'Missing client secret',
})
}
const client = await db.client.findFirst({
where: {
secret,
},
})
if(!client) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'Invalid client secret',
})
}
req.client = {
project_id: client.project_id,
}
next()
}

View File

@@ -0,0 +1,13 @@
import { MixanIssue, MixanIssuesResponse } from "@mixan/types";
export function issues(arr: Array<MixanIssue>): MixanIssuesResponse {
return {
issues: arr.map((item) => {
return {
field: item.field,
message: item.message,
value: item.value,
};
})
}
}

View File

@@ -0,0 +1,8 @@
import { MixanResponse } from "@mixan/types";
export function success<T>(result?: T): MixanResponse<T | null> {
return {
result: result || null,
status: 'ok'
}
}

View File

@@ -0,0 +1,34 @@
import {Router} from 'express'
import { db } from '../db';
import { MixanRequest } from '../types/express';
import { EventPayload } from '@mixan/types';
import { getEvents, getProfileIdFromEvents } from '../services/event';
import { success } from '../responses/success';
const router = Router();
type PostRequest = MixanRequest<Array<EventPayload>>
router.get('/events', async (req, res) => {
const events = await getEvents(req.client.project_id)
res.json(success(events))
})
router.post('/events', async (req: PostRequest, res) => {
const projectId = req.client.project_id
const profileId = await getProfileIdFromEvents(projectId, req.body)
await db.event.createMany({
data: req.body.map(event => ({
name: event.name,
properties: event.properties,
createdAt: event.time,
project_id: projectId,
profile_id: profileId,
}))
})
res.status(201).json(success())
})
export default router

View File

@@ -0,0 +1,133 @@
import { Router } from 'express'
import { db } from '../db'
import { MixanRequest } from '../types/express'
import {
createProfile,
getProfileByExternalId,
updateProfile,
} from '../services/profile'
import {
ProfileDecrementPayload,
ProfileIncrementPayload,
ProfilePayload,
} from '@mixan/types'
import { issues } from '../responses/errors'
import { success } from '../responses/success'
const router = Router()
type PostRequest = MixanRequest<ProfilePayload>
router.get('/profiles', async (req, res) => {
res.json(success(await db.profile.findMany({
where: {
project_id: req.client.project_id,
},
})))
})
router.post('/profiles', async (req: PostRequest, res) => {
const body = req.body
const projectId = req.client.project_id
const profile = await getProfileByExternalId(projectId, body.id)
if (profile) {
await updateProfile(projectId, body.id, body, profile)
} else {
await createProfile(projectId, body)
}
res.status(profile ? 200 : 201).json(success())
})
router.post(
'/profiles/increment',
async (req: MixanRequest<ProfileIncrementPayload>, res) => {
const body = req.body
const projectId = req.client.project_id
const profile = await getProfileByExternalId(projectId, body.id)
if (profile) {
const existingProperties = (
typeof profile.properties === 'object' ? profile.properties || {} : {}
) as Record<string, number>
const value =
body.name in existingProperties ? existingProperties[body.name] : 0
const properties = {
...existingProperties,
[body.name]: value + body.value,
}
if (typeof value !== 'number') {
return res.status(400).json(
issues([
{
field: 'name',
message: 'Property is not a number',
value,
},
])
)
}
await db.profile.updateMany({
where: {
external_id: String(body.id),
project_id: req.client.project_id,
},
data: {
properties,
},
})
}
res.status(200).json(success())
}
)
router.post(
'/profiles/decrement',
async (req: MixanRequest<ProfileDecrementPayload>, res) => {
const body = req.body
const projectId = req.client.project_id
const profile = await getProfileByExternalId(projectId, body.id)
if (profile) {
const existingProperties = (
typeof profile.properties === 'object' ? profile.properties || {} : {}
) as Record<string, number>
const value =
body.name in existingProperties ? existingProperties[body.name] : 0
if (typeof value !== 'number') {
return res.status(400).json(
issues([
{
field: 'name',
message: 'Property is not a number',
value,
},
])
)
}
const properties = {
...existingProperties,
[body.name]: value - body.value,
}
await db.profile.updateMany({
where: {
external_id: String(body.id),
project_id: req.client.project_id,
},
data: {
properties,
},
})
}
res.status(200).json(success())
}
)
export default router

View File

@@ -0,0 +1,29 @@
import { EventPayload } from "@mixan/types";
import { db } from "../db";
export function getEvents(projectId: string) {
return db.event.findMany({
where: {
project_id: projectId,
}
})
}
export async function getProfileIdFromEvents(projectId: string, events: EventPayload[]) {
const event = events.find(item => !!item.externalId)
if(event?.externalId) {
return db.profile.findUnique({
where: {
project_id_external_id: {
project_id: projectId,
external_id: event.externalId,
}
}
}).then((res) => {
return res?.id || null
}).catch(() => {
return null
})
}
return null
}

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

@@ -0,0 +1,75 @@
import { EventPayload, ProfilePayload } from "@mixan/types";
import { db } from "../db";
import { Prisma } from "@prisma/client";
export function createProfile(projectId: string, payload: ProfilePayload) {
const { id, email, first_name, last_name, avatar, properties } = payload
return db.profile.create({
data:{
external_id: id,
email,
first_name,
last_name,
avatar,
properties: properties || {},
project_id: projectId,
}
})
}
type DbProfile = Exclude<Prisma.PromiseReturnType<typeof getProfileByExternalId>, null>
export function getProfileByExternalId(projectId: string, externalId: string) {
return db.profile.findUnique({
where: {
project_id_external_id: {
project_id: projectId,
external_id: externalId,
}
}
})
}
export function getProfiles(projectId: string) {
return db.profile.findMany({
where: {
project_id: projectId,
}
})
}
export async function updateProfile(projectId: string, profileId: string, payload: Omit<ProfilePayload, 'id'>, oldProfile: DbProfile) {
const { email, first_name, last_name, avatar, properties } = payload
return db.profile.update({
where: {
project_id_external_id: {
project_id: projectId,
external_id: profileId,
}
},
data: {
email,
first_name,
last_name,
avatar,
properties: {
...(typeof oldProfile.properties === 'object' ? oldProfile.properties || {} : {}),
...(properties || {}),
},
},
})
}
export async function getInternalProfileId(profileId?: string | null) {
if(!profileId) {
return null
}
const profile = await db.profile.findFirst({
where: {
external_id: profileId,
}
})
return profile?.id || null
}

View File

@@ -0,0 +1,3 @@
export type MixanRequest<Body> = Omit<Express.Request,'body'> & {
body: Body
}