Create backend for Dashboards
This commit is contained in:
parent
1c73d21925
commit
35aa6b733d
13
common/dashboard.ts
Normal file
13
common/dashboard.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { JSONContent } from '@tiptap/core'
|
||||
|
||||
export type Dashboard = {
|
||||
id: string
|
||||
name: string
|
||||
content: JSONContent
|
||||
creatorId: string // User id
|
||||
createdTime: number
|
||||
lastUpdatedTime: number
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const MAX_DASHBOARD_NAME_LENGTH = 75
|
|
@ -169,5 +169,14 @@ service cloud.firestore {
|
|||
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isMember();
|
||||
}
|
||||
}
|
||||
|
||||
match /dashboards/{dashboardId} {
|
||||
allow read;
|
||||
allow update: if request.auth.uid == resource.data.creatorId
|
||||
&& request.resource.data.diff(resource.data)
|
||||
.affectedKeys()
|
||||
.hasOnly(['name', 'content']);
|
||||
allow delete: if request.auth.uid == resource.data.creatorId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
86
functions/src/create-dashboard.ts
Normal file
86
functions/src/create-dashboard.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { getUser } from './utils'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { slugify } from '../../common/util/slugify'
|
||||
import { randomString } from '../../common/util/random'
|
||||
import { Dashboard, MAX_DASHBOARD_NAME_LENGTH } from '../../common/dashboard'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { z } from 'zod'
|
||||
|
||||
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
|
||||
z.intersection(
|
||||
z.record(z.any()),
|
||||
z.object({
|
||||
type: z.string().optional(),
|
||||
attrs: z.record(z.any()).optional(),
|
||||
content: z.array(contentSchema).optional(),
|
||||
marks: z
|
||||
.array(
|
||||
z.intersection(
|
||||
z.record(z.any()),
|
||||
z.object({
|
||||
type: z.string(),
|
||||
attrs: z.record(z.any()).optional(),
|
||||
})
|
||||
)
|
||||
)
|
||||
.optional(),
|
||||
text: z.string().optional(),
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const dashboardSchema = z.object({
|
||||
name: z.string().min(1).max(MAX_DASHBOARD_NAME_LENGTH),
|
||||
content: contentSchema,
|
||||
})
|
||||
|
||||
export const createdashboard = newEndpoint({}, async (req, auth) => {
|
||||
const firestore = admin.firestore()
|
||||
const { name, content } = validate(dashboardSchema, req.body)
|
||||
|
||||
const creator = await getUser(auth.uid)
|
||||
if (!creator)
|
||||
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
||||
|
||||
console.log('creating dashboard for', creator.username, 'named', name)
|
||||
|
||||
const slug = await getSlug(name)
|
||||
|
||||
const dashboardRef = firestore.collection('dashboards').doc()
|
||||
|
||||
const dashboard: Dashboard = {
|
||||
id: dashboardRef.id,
|
||||
creatorId: creator.id,
|
||||
slug,
|
||||
name,
|
||||
createdTime: Date.now(),
|
||||
content: content,
|
||||
}
|
||||
|
||||
await dashboardRef.create(dashboard)
|
||||
|
||||
return { status: 'success', dashboard: dashboard }
|
||||
})
|
||||
|
||||
export const getSlug = async (name: string) => {
|
||||
const proposedSlug = slugify(name)
|
||||
|
||||
const preexistingDashboard = await getDashboardFromSlug(proposedSlug)
|
||||
|
||||
return preexistingDashboard
|
||||
? proposedSlug + '-' + randomString()
|
||||
: proposedSlug
|
||||
}
|
||||
|
||||
export async function getDashboardFromSlug(slug: string) {
|
||||
const firestore = admin.firestore()
|
||||
const snap = await firestore
|
||||
.collection('dashboards')
|
||||
.where('slug', '==', slug)
|
||||
.get()
|
||||
|
||||
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
||||
}
|
|
@ -71,6 +71,7 @@ import { stripewebhook, createcheckoutsession } from './stripe'
|
|||
import { getcurrentuser } from './get-current-user'
|
||||
import { acceptchallenge } from './accept-challenge'
|
||||
import { getcustomtoken } from './get-custom-token'
|
||||
import { createdashboard } from './create-dashboard'
|
||||
|
||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||
return onRequest(opts, handler as any)
|
||||
|
@ -96,6 +97,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
|||
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
||||
const createDashboardFunction = toCloudFunction(createdashboard)
|
||||
|
||||
export {
|
||||
healthFunction as health,
|
||||
|
@ -119,4 +121,5 @@ export {
|
|||
getCurrentUserFunction as getcurrentuser,
|
||||
acceptChallenge as acceptchallenge,
|
||||
getCustomTokenFunction as getcustomtoken,
|
||||
createDashboardFunction as createdashboard,
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import { unsubscribe } from './unsubscribe'
|
|||
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||
import { getcurrentuser } from './get-current-user'
|
||||
import { getcustomtoken } from './get-custom-token'
|
||||
import { createdashboard } from './create-dashboard'
|
||||
|
||||
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
||||
const app = express()
|
||||
|
@ -67,6 +68,7 @@ addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
|
|||
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
||||
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
||||
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
||||
addEndpointRoute('/createdashboard', createdashboard)
|
||||
|
||||
app.listen(PORT)
|
||||
console.log(`Serving functions on port ${PORT}.`)
|
||||
|
|
|
@ -4,6 +4,7 @@ import { chunk } from 'lodash'
|
|||
import { Contract } from '../../common/contract'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import { Group } from '../../common/group'
|
||||
import { Dashboard } from 'common/dashboard'
|
||||
|
||||
export const log = (...args: unknown[]) => {
|
||||
console.log(`[${new Date().toISOString()}]`, ...args)
|
||||
|
@ -80,6 +81,10 @@ export const getGroup = (groupId: string) => {
|
|||
return getDoc<Group>('groups', groupId)
|
||||
}
|
||||
|
||||
export const getDashboard = (dashboardId: string) => {
|
||||
return getDoc<Dashboard>('dashboards', dashboardId)
|
||||
}
|
||||
|
||||
export const getUser = (userId: string) => {
|
||||
return getDoc<User>('users', userId)
|
||||
}
|
||||
|
|
|
@ -88,3 +88,7 @@ export function acceptChallenge(params: any) {
|
|||
export function getCurrentUser(params: any) {
|
||||
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
||||
}
|
||||
|
||||
export function createDashboard(params: any) {
|
||||
return call(getFunctionUrl('createdashboard'), 'POST', params)
|
||||
}
|
||||
|
|
24
web/lib/firebase/dashboards.ts
Normal file
24
web/lib/firebase/dashboards.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { deleteDoc, doc, updateDoc } from 'firebase/firestore'
|
||||
import { Dashboard } from 'common/dashboard'
|
||||
import { coll, getValue } from './utils'
|
||||
|
||||
export const dashboards = coll<Dashboard>('dashboards')
|
||||
|
||||
export function dashboardPath(dashboardSlug: string) {
|
||||
return `/dashboard/${dashboardSlug}}`
|
||||
}
|
||||
|
||||
export function updateDashboard(
|
||||
dashboard: Dashboard,
|
||||
updates: Partial<Dashboard>
|
||||
) {
|
||||
return updateDoc(doc(dashboards, dashboard.id), updates)
|
||||
}
|
||||
|
||||
export function deleteDashboard(dashboard: Dashboard) {
|
||||
return deleteDoc(doc(dashboards, dashboard.id))
|
||||
}
|
||||
|
||||
export function getDashboard(dashboardId: string) {
|
||||
return getValue<Dashboard>(doc(dashboards, dashboardId))
|
||||
}
|
Loading…
Reference in New Issue
Block a user