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();
|
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 { getcurrentuser } from './get-current-user'
|
||||||
import { acceptchallenge } from './accept-challenge'
|
import { acceptchallenge } from './accept-challenge'
|
||||||
import { getcustomtoken } from './get-custom-token'
|
import { getcustomtoken } from './get-custom-token'
|
||||||
|
import { createdashboard } from './create-dashboard'
|
||||||
|
|
||||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||||
return onRequest(opts, handler as any)
|
return onRequest(opts, handler as any)
|
||||||
|
@ -96,6 +97,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
||||||
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||||
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
||||||
|
const createDashboardFunction = toCloudFunction(createdashboard)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
healthFunction as health,
|
healthFunction as health,
|
||||||
|
@ -119,4 +121,5 @@ export {
|
||||||
getCurrentUserFunction as getcurrentuser,
|
getCurrentUserFunction as getcurrentuser,
|
||||||
acceptChallenge as acceptchallenge,
|
acceptChallenge as acceptchallenge,
|
||||||
getCustomTokenFunction as getcustomtoken,
|
getCustomTokenFunction as getcustomtoken,
|
||||||
|
createDashboardFunction as createdashboard,
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { unsubscribe } from './unsubscribe'
|
||||||
import { stripewebhook, createcheckoutsession } from './stripe'
|
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||||
import { getcurrentuser } from './get-current-user'
|
import { getcurrentuser } from './get-current-user'
|
||||||
import { getcustomtoken } from './get-custom-token'
|
import { getcustomtoken } from './get-custom-token'
|
||||||
|
import { createdashboard } from './create-dashboard'
|
||||||
|
|
||||||
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
||||||
const app = express()
|
const app = express()
|
||||||
|
@ -67,6 +68,7 @@ addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
|
||||||
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
||||||
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
||||||
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
||||||
|
addEndpointRoute('/createdashboard', createdashboard)
|
||||||
|
|
||||||
app.listen(PORT)
|
app.listen(PORT)
|
||||||
console.log(`Serving functions on port ${PORT}.`)
|
console.log(`Serving functions on port ${PORT}.`)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { chunk } from 'lodash'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { PrivateUser, User } from '../../common/user'
|
import { PrivateUser, User } from '../../common/user'
|
||||||
import { Group } from '../../common/group'
|
import { Group } from '../../common/group'
|
||||||
|
import { Dashboard } from 'common/dashboard'
|
||||||
|
|
||||||
export const log = (...args: unknown[]) => {
|
export const log = (...args: unknown[]) => {
|
||||||
console.log(`[${new Date().toISOString()}]`, ...args)
|
console.log(`[${new Date().toISOString()}]`, ...args)
|
||||||
|
@ -80,6 +81,10 @@ export const getGroup = (groupId: string) => {
|
||||||
return getDoc<Group>('groups', groupId)
|
return getDoc<Group>('groups', groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDashboard = (dashboardId: string) => {
|
||||||
|
return getDoc<Dashboard>('dashboards', dashboardId)
|
||||||
|
}
|
||||||
|
|
||||||
export const getUser = (userId: string) => {
|
export const getUser = (userId: string) => {
|
||||||
return getDoc<User>('users', userId)
|
return getDoc<User>('users', userId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,3 +88,7 @@ export function acceptChallenge(params: any) {
|
||||||
export function getCurrentUser(params: any) {
|
export function getCurrentUser(params: any) {
|
||||||
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
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