Skip to content

Commit

Permalink
Add cms.user() api to retrieve logged in user from the previewed website
Browse files Browse the repository at this point in the history
  • Loading branch information
benmerckx committed Jan 26, 2024
1 parent 0e8b06c commit 15d5f04
Show file tree
Hide file tree
Showing 13 changed files with 79 additions and 31 deletions.
6 changes: 3 additions & 3 deletions src/backend/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class Handler implements Resolver {
this.parsePreview.bind(this)
)
this.changes = new ChangeSetCreator(options.config)
const auth = options.auth || Auth.anonymous()
const auth = options.auth ?? Auth.anonymous()
this.connect = ctx => new HandlerConnection(this, ctx)
this.router = createRouter(auth, this.connect)
}
Expand Down Expand Up @@ -192,8 +192,8 @@ class HandlerConnection implements Connection {
previewToken(): Promise<string> {
const {previews} = this.handler.options
const user = this.ctx.user
if (!user) return previews.sign({anonymous: true})
return previews.sign({sub: user.sub})
if (!user) throw new Error('Unauthorized, user not available')
return previews.sign(user)
}

// Media
Expand Down
6 changes: 3 additions & 3 deletions src/backend/Previews.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type PreviewTokenPayload = {sub: string} | {anonymous: true}
import {User} from 'alinea/core'

export interface Previews {
sign(data: PreviewTokenPayload): Promise<string>
verify(token: string): Promise<PreviewTokenPayload>
sign(data: User): Promise<string>
verify(token: string): Promise<User>
}
7 changes: 4 additions & 3 deletions src/backend/util/JWTPreviews.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {User} from 'alinea/core'
import {sign, verify} from 'alinea/core/util/JWT'
import {PreviewTokenPayload, Previews} from '../Previews.js'
import {Previews} from '../Previews.js'

export class JWTPreviews implements Previews {
constructor(private secret: string) {}

sign(data: PreviewTokenPayload): Promise<string> {
sign(data: User): Promise<string> {
return sign(data, this.secret)
}

verify(token: string): Promise<PreviewTokenPayload> {
verify(token: string): Promise<User> {
return verify(token, this.secret)
}
}
19 changes: 17 additions & 2 deletions src/cli/Serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {Handler} from 'alinea/backend/Handler'
import {HttpRouter} from 'alinea/backend/router/Router'
import {createCloudDebugHandler} from 'alinea/cloud/server/CloudDebugHandler'
import {createCloudHandler} from 'alinea/cloud/server/CloudHandler'
import {Auth, localUser} from 'alinea/core'
import {CMS} from 'alinea/core/CMS'
import {BuildOptions} from 'esbuild'
import path from 'node:path'
import simpleGit from 'simple-git'
import pkg from '../../package.json'
import {generate} from './Generate.js'
import {buildOptions} from './build/BuildOptions.js'
Expand Down Expand Up @@ -85,15 +87,27 @@ export async function serve(options: ServeOptions): Promise<void> {
let cms: CMS | undefined
let handle: HttpRouter | undefined

const git = simpleGit(rootDir)
const [name = localUser.name, email] = (
await Promise.all([git.getConfig('user.name'), git.getConfig('user.email')])
).map(res => res.value ?? undefined)
const user = {...localUser, name, email}

while (true) {
const current = await nextGen
if (!current?.value) return
const {cms: currentCMS, localData: fileData, db} = current.value
if (currentCMS === cms) {
context.liveReload.reload('refetch')
} else {
const history = new GitHistory(git, currentCMS.config, rootDir)
const auth: Auth.Server = {
async contextFor() {
return {user}
}
}
const backend = createBackend()
handle = createLocalServer(context, backend)
handle = createLocalServer(context, backend, user)
cms = currentCMS
context.liveReload.reload('refresh')

Expand All @@ -107,12 +121,13 @@ export async function serve(options: ServeOptions): Promise<void> {
process.env.ALINEA_API_KEY
)
return new Handler({
auth,
config: currentCMS.config,
db,
target: fileData,
media: fileData,
drafts,
history: new GitHistory(currentCMS.config, rootDir),
history,
previews: new JWTPreviews('dev'),
previewAuthToken: 'dev'
})
Expand Down
6 changes: 4 additions & 2 deletions src/cli/serve/CreateLocalServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {ReadableStream, Request, Response, TextEncoderStream} from '@alinea/iso'
import {Handler} from 'alinea/backend'
import {HttpRouter, router} from 'alinea/backend/router/Router'
import {cloudUrl} from 'alinea/cloud/server/CloudConfig'
import {Trigger, trigger} from 'alinea/core'
import {Trigger, User, trigger} from 'alinea/core'
import esbuild, {BuildOptions, BuildResult, OutputFile} from 'esbuild'
import fs from 'node:fs'
import path from 'node:path'
Expand Down Expand Up @@ -66,7 +66,8 @@ export function createLocalServer(
production,
liveReload
}: ServeContext,
handler: Handler
handler: Handler,
user: User
): HttpRouter {
const devDir = path.join(staticDir, 'dev')
const matcher = router.matcher()
Expand Down Expand Up @@ -95,6 +96,7 @@ export function createLocalServer(
plugins: buildOptions?.plugins || [],
inject: ['alinea/cli/util/WarnPublicEnv'],
define: {
'process.env.ALINEA_USER': JSON.stringify(JSON.stringify(user)),
'process.env.NODE_ENV': production ? "'production'" : "'development'",
'process.env.ALINEA_CLOUD_URL': cloudUrl
? JSON.stringify(cloudUrl)
Expand Down
12 changes: 6 additions & 6 deletions src/cli/serve/GitHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import {History, Revision} from 'alinea/backend/History'
import {Config} from 'alinea/core'
import {EntryRecord} from 'alinea/core/EntryRecord'
import {join} from 'alinea/core/util/Paths'
import simpleGit, {SimpleGit} from 'simple-git'
import {SimpleGit} from 'simple-git'

const encoder = new TextEncoder()

export class GitHistory implements History {
git: SimpleGit

constructor(public config: Config, public rootDir: string) {
this.git = simpleGit(rootDir)
}
constructor(
public git: SimpleGit,
public config: Config,
public rootDir: string
) {}

async revisions(file: string): Promise<Array<Revision>> {
const list = await this.git.log([
Expand Down
3 changes: 2 additions & 1 deletion src/cloud/server/CloudDebugHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {GitHistory} from 'alinea/cli/serve/GitHistory'
import {Config, Connection, Draft, createId} from 'alinea/core'
import {EntryRecord} from 'alinea/core/EntryRecord'
import {Mutation} from 'alinea/core/Mutation'
import simpleGit from 'simple-git'

const latency = 0

Expand All @@ -17,7 +18,7 @@ export class DebugCloud implements Media, Target, History, Drafts, Pending {
history: History

constructor(public config: Config, public db: Database, rootDir: string) {
this.history = new GitHistory(config, rootDir)
this.history = new GitHistory(simpleGit(rootDir), config, rootDir)
}

async mutate(params: Connection.MutateParams) {
Expand Down
3 changes: 2 additions & 1 deletion src/core/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {Route} from 'alinea/backend/router/Router'
import type {ComponentType} from 'react'
import {Connection} from './Connection.js'
import {Session} from './Session.js'
import {localUser} from './User.js'

export namespace Auth {
export type Server = {
Expand All @@ -14,7 +15,7 @@ export namespace Auth {
export function anonymous(): Auth.Server {
return {
async contextFor() {
return {}
return {user: localUser}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/core/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export interface User {
email?: string
name?: string
}

export const localUser = {
sub: 'local',
name: 'Local user'
}
30 changes: 23 additions & 7 deletions src/core/driver/NextDriver.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {Entry} from '../Entry.js'
import {EntryPhase} from '../EntryRow.js'
import {outcome} from '../Outcome.js'
import {ResolveDefaults, Resolver} from '../Resolver.js'
import {User} from '../User.js'
import {createSelection} from '../pages/CreateSelection.js'
import {Realm} from '../pages/Realm.js'
import {DefaultDriver} from './DefaultDriver.server.js'
Expand All @@ -26,10 +27,27 @@ const SearchParams = object({
realm: enums(Realm)
})

const PREVIEW_TOKEN = 'alinea-preview-token'

class NextDriver extends DefaultDriver implements NextApi {
apiKey = process.env.ALINEA_API_KEY
jwtSecret = this.apiKey || 'dev'

async user(): Promise<User | null> {
const {cookies, draftMode} = await import('next/headers.js')
const [draftStatus] = outcome(() => draftMode())
const isDraft = draftStatus?.isEnabled
// We exit early if we're not in draft mode to avoid marking every page
// as dynamic
if (!isDraft) return null
const token = cookies().get(PREVIEW_TOKEN)?.value
if (!token) return null
const previews = new JWTPreviews(this.jwtSecret)
const payload = await previews.verify(token)
if (!payload) return null
return payload
}

async getDefaults(): Promise<ResolveDefaults> {
const {cookies, draftMode} = await import('next/headers.js')
const [draftStatus] = outcome(() => draftMode())
Expand Down Expand Up @@ -92,7 +110,8 @@ class NextDriver extends DefaultDriver implements NextApi {
realm: searchParams.get('realm')
})
const previews = new JWTPreviews(this.jwtSecret)
const payload = await previews.verify(params.token)
await previews.verify(params.token)
cookies().set(PREVIEW_TOKEN, params.token)
if (!searchParams.has('full')) {
// Clear preview cookies
cookies().delete(PREVIEW_UPDATE_NAME)
Expand Down Expand Up @@ -120,17 +139,14 @@ class NextDriver extends DefaultDriver implements NextApi {
})
}

async previews(): Promise<JSX.Element | null> {
previews = async (): Promise<JSX.Element | null> => {
const {draftMode} = await import('next/headers.js')
const [draftStatus] = outcome(() => draftMode())
const isDraft = draftStatus?.isEnabled
if (!isDraft) return null
const user = await this.user()
const NextPreviews = lazy(() => import('alinea/core/driver/NextPreviews'))
return (
<Suspense>
<NextPreviews />
</Suspense>
)
return <Suspense>{user && <NextPreviews user={user} />}</Suspense>
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/driver/NextDriver.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {CMSApi} from '../CMS.js'
import {Config} from '../Config.js'
import {User} from '../User.js'
import {DefaultDriver} from './DefaultDriver.js'

export interface NextApi extends CMSApi {
user(): Promise<User | null>
previews(): Promise<JSX.Element | null>
backendHandler(request: Request): Promise<Response>
previewHandler(request: Request): Promise<Response>
Expand Down
7 changes: 6 additions & 1 deletion src/core/driver/NextPreviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import {
import {usePreview} from 'alinea/preview/react'
// @ts-ignore
import {useRouter} from 'next/navigation'
import {User} from '../User.js'

const MAX_CHUNKS = 5

export default function NextPreviews() {
export interface NextPreviewsProps {
user: User
}

export default function NextPreviews({user}: NextPreviewsProps) {
const router = useRouter()
const {isPreviewing} = usePreview({
async preview({entryId, phase, update}) {
Expand Down
4 changes: 2 additions & 2 deletions src/dashboard/atoms/DashboardAtoms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Connection, Session} from 'alinea/core'
import {Connection, Session, User} from 'alinea/core'
import {atom, useAtomValue, useSetAtom} from 'jotai'
import {useHydrateAtoms} from 'jotai/utils'
import {useEffect} from 'react'
Expand All @@ -19,7 +19,7 @@ export function useSetDashboardOptions(options: AppProps) {
[
sessionAtom,
{
user: {sub: 'anonymous'},
user: JSON.parse(process.env.ALINEA_USER as string) as User,
cnx: client
}
]
Expand Down

0 comments on commit 15d5f04

Please sign in to comment.