Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion apps/blog-next/app/api/auth/clerk/logout/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { provider } from '@holo-js/auth'
import { logoutWithClerk } from '@holo-js/auth-clerk'

function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}

export async function POST(request: Request) {
const result = await logoutWithClerk(request)
let currentProvider: string | null
try {
currentProvider = await provider()
} catch (error) {
return Response.json({ ok: false, error: getErrorMessage(error) }, { status: 500 })
}

if (currentProvider !== 'clerk') {
return Response.redirect(new URL('/', request.url), 303)
}

let result: Awaited<ReturnType<typeof logoutWithClerk>>
try {
result = await logoutWithClerk(request)
} catch (error) {
return Response.json({ ok: false, error: getErrorMessage(error) }, { status: 500 })
}

if (!result.ok) {
return Response.json(result, { status: 422 })
}
Expand Down
14 changes: 9 additions & 5 deletions apps/blog-next/app/api/auth/user/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { check, user } from '@holo-js/auth'
import auth, { check, provider, user } from '@holo-js/auth'

export async function GET(request: Request) {
const guard = new URL(request.url).searchParams.get('guard') ?? undefined
const guardAuth = guard ? auth.guard(guard) : undefined

export async function GET() {
return Response.json({
authenticated: await check(),
guard: 'web',
user: await user(),
authenticated: guardAuth ? await guardAuth.check() : await check(),
guard: guard ?? 'web',
provider: guardAuth ? await guardAuth.provider() : await provider(),
user: guardAuth ? await guardAuth.user() : await user(),
})
}
24 changes: 23 additions & 1 deletion apps/blog-next/app/api/auth/workos/logout/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { provider } from '@holo-js/auth'
import { logoutWithWorkos } from '@holo-js/auth-workos'

function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}

export async function POST(request: Request) {
const result = await logoutWithWorkos(request)
let currentProvider: string | null
try {
currentProvider = await provider()
} catch (error) {
return Response.json({ ok: false, error: getErrorMessage(error) }, { status: 500 })
}

if (currentProvider !== 'workos') {
return Response.redirect(new URL('/', request.url), 303)
}

let result: Awaited<ReturnType<typeof logoutWithWorkos>>
try {
result = await logoutWithWorkos(request)
} catch (error) {
return Response.json({ ok: false, error: getErrorMessage(error) }, { status: 500 })
}

if (!result.ok) {
return Response.json(result, { status: 422 })
}
Expand Down
32 changes: 32 additions & 0 deletions apps/blog-next/app/api/super-admin/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import auth from '@holo-js/auth'
import { validate } from '@holo-js/forms'

import { loginForm } from '@/lib/schemas/auth'

export async function POST(request: Request) {
const submission = await validate(request, loginForm, {
throttle: 'login',
})

if (!submission.valid) {
return Response.json(submission.fail(), {
status: submission.fail().status,
})
}

const { data: session, error } = await auth.guard('admin').login(submission.data)
if (error) {
const failure = submission.fail({
status: error.status,
errors: error.fields,
})

return Response.json(failure, { status: failure.status })
}

return Response.json(submission.success({
message: 'Signed in as super admin.',
redirectTo: '/super-admin',
user: session.user,
}))
}
13 changes: 13 additions & 0 deletions apps/blog-next/app/api/super-admin/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import auth from '@holo-js/auth'

export async function POST() {
const admin = auth.guard('admin')
await admin.logout()

return Response.json({
ok: true,
authenticated: false,
message: 'Signed out of super admin.',
user: await admin.user(),
})
}
16 changes: 10 additions & 6 deletions apps/blog-next/app/auth-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,16 @@ export function AuthNav() {
<>
<span style={{ color: '#e5eef8' }}>{displayName}</span>
<button type="button" disabled={isLoggingOut} onClick={logout} style={logoutButtonStyle}>Logout</button>
<form action="/api/auth/workos/logout" method="post" style={logoutFormStyle}>
<button type="submit" style={logoutButtonStyle}>Logout from WorkOS</button>
</form>
<form action="/api/auth/clerk/logout" method="post" style={logoutFormStyle}>
<button type="submit" style={logoutButtonStyle}>Logout from Clerk</button>
</form>
{auth.provider === 'workos' && (
<form action="/api/auth/workos/logout" method="post" style={logoutFormStyle}>
<button type="submit" style={logoutButtonStyle}>Logout from WorkOS</button>
</form>
)}
{auth.provider === 'clerk' && (
<form action="/api/auth/clerk/logout" method="post" style={logoutFormStyle}>
<button type="submit" style={logoutButtonStyle}>Logout from Clerk</button>
</form>
)}
</>
)
}
3 changes: 2 additions & 1 deletion apps/blog-next/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export default async function RootLayout({ children }: { children: ReactNode })
<Link href="/" style={{ color: '#fff', textDecoration: 'none', fontWeight: 700 }}>blog-next</Link>
<Link href="/posts" style={{ color: '#cbd5e1', textDecoration: 'none' }}>Posts</Link>
<Link href="/admin" style={{ color: '#cbd5e1', textDecoration: 'none' }}>Admin</Link>
<AuthProvider initialUser={currentAuth.user}>
<Link href="/super-admin" style={{ color: '#cbd5e1', textDecoration: 'none' }}>Super Admin</Link>
<AuthProvider initialProvider={currentAuth.provider} initialUser={currentAuth.user}>
<AuthNav />
</AuthProvider>
</nav>
Expand Down
93 changes: 93 additions & 0 deletions apps/blog-next/app/super-admin/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client'

import { useRouter } from 'next/navigation'
import { useAuth } from '@holo-js/auth/next/client'
import { useForm } from '@holo-js/adapter-next/client'
import { loginForm } from '@/lib/schemas/auth'

const panelStyle = {
display: 'grid',
gap: '1rem',
maxWidth: '32rem',
padding: '1.5rem',
borderRadius: '1rem',
background: '#111827',
border: '1px solid rgba(148, 163, 184, 0.16)',
} satisfies React.CSSProperties

export default function SuperAdminLoginPage() {
const router = useRouter()
const auth = useAuth({ guard: 'admin' })
const form = useForm(loginForm, {
validateOn: 'blur',
initialValues: { email: '', password: '', remember: false },
async submitter({ formData }) {
const response = await fetch('/api/super-admin/login', { method: 'POST', body: formData })
const submission = await response.json()
if (submission?.ok === true && typeof submission.data?.redirectTo === 'string') {
try {
await auth.refreshUser()
} catch (error) {
console.warn('Admin auth refresh failed after login.', error)
}
router.replace(submission.data.redirectTo)
}
return submission
},
})

return (
<section style={panelStyle}>
<div>
<h1 style={{ margin: '0 0 0.5rem 0' }}>Super Admin Sign In</h1>
<p style={{ margin: 0, color: '#94a3b8' }}>Use an admin account to access the super admin area.</p>
</div>

<form onSubmit={(event) => { event.preventDefault(); form.submit() }} style={{ display: 'grid', gap: '0.9rem' }}>
<label style={{ display: 'grid', gap: '0.35rem' }}>
<span>Email</span>
<input
name="email"
type="email"
value={form.values.email}
onInput={(event) => form.fields.email.onInput(event.currentTarget.value)}
onBlur={() => form.fields.email.onBlur()}
/>
{form.errors.has('email') ? <span style={{ color: '#fca5a5' }}>{form.errors.first('email')}</span> : null}
</label>

<label style={{ display: 'grid', gap: '0.35rem' }}>
<span>Password</span>
<input
name="password"
type="password"
value={form.values.password}
onInput={(event) => form.fields.password.onInput(event.currentTarget.value)}
onBlur={() => form.fields.password.onBlur()}
/>
{form.errors.has('password') ? <span style={{ color: '#fca5a5' }}>{form.errors.first('password')}</span> : null}
</label>

<label style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<input
name="remember"
type="checkbox"
checked={form.values.remember}
onChange={(event) => form.fields.remember.onInput(event.currentTarget.checked)}
/>
Remember me
</label>

<button type="submit" disabled={form.submitting}>
{form.submitting ? 'Signing in...' : 'Sign in as super admin'}
</button>
</form>

{form.lastSubmission?.ok === true ? (
<div style={{ color: '#86efac' }}>
<p style={{ margin: 0 }}>Signed in as super admin.</p>
</div>
) : null}
</section>
)
}
39 changes: 39 additions & 0 deletions apps/blog-next/app/super-admin/logout-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@holo-js/auth/next/client'

export function SuperAdminLogoutButton() {
const router = useRouter()
const auth = useAuth({ guard: 'admin' })
const [isLoggingOut, setIsLoggingOut] = useState(false)

async function logout() {
if (isLoggingOut) {
return
}

setIsLoggingOut(true)
try {
const response = await fetch('/api/super-admin/logout', { method: 'POST' })
if (!response.ok) {
console.warn('Super admin logout failed.', { status: response.status })
return
}

await auth.refreshUser()
router.replace('/super-admin/login')
} catch (error) {
console.warn('Super admin logout failed.', error)
} finally {
setIsLoggingOut(false)
}
}

return (
<button type="button" onClick={logout} disabled={isLoggingOut}>
{isLoggingOut ? 'Signing out...' : 'Sign out of super admin'}
</button>
)
}
24 changes: 24 additions & 0 deletions apps/blog-next/app/super-admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { redirect } from 'next/navigation'
import { auth } from '@holo-js/auth/next/server'
import { SuperAdminLogoutButton } from './logout-button'

export default async function SuperAdminPage() {
const currentAuth = await auth({ guard: 'admin' })

if (!currentAuth.authenticated) {
redirect('/super-admin/login')
}

const displayName = currentAuth.user?.name ?? currentAuth.user?.email ?? 'Super Admin'

return (
<section style={{ display: 'grid', gap: '0.75rem', maxWidth: '42rem' }}>
<p style={{ margin: 0, color: '#7dd3fc', fontSize: '0.875rem', textTransform: 'uppercase' }}>Admin guard</p>
<h1 style={{ margin: 0 }}>Super Admin</h1>
<p style={{ margin: 0, color: '#cbd5e1' }}>Signed in as {displayName} through the admin guard.</p>
<div>
<SuperAdminLogoutButton />
</div>
</section>
)
}
16 changes: 8 additions & 8 deletions apps/blog-next/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ export default defineAuthConfig({
driver: 'session',
provider: 'users',
},
// admin: {
// driver: 'session',
// provider: 'admins',
// },
admin: {
driver: 'session',
provider: 'admins',
},
},
providers: {
users: {
model: 'User',
identifiers: ['email'],
},
// admins: {
// model: 'Admin',
// identifiers: ['email'],
// },
admins: {
model: 'Admin',
identifiers: ['email'],
},
},
passwords: {
users: {
Expand Down
12 changes: 11 additions & 1 deletion apps/blog-next/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ export const proxy = protectRoutes(
routes: ['/admin/*'],
redirectTo: '/login',
}),
guestOnly({
routes: ['/super-admin/login'],
guard: 'admin',
redirectTo: '/super-admin',
}),
authOnly({
routes: ['/super-admin'],
guard: 'admin',
redirectTo: '/super-admin/login',
}),
)

export const config = {
matcher: ['/login', '/register', '/forgot-password', '/reset-password', '/admin/:path*'],
matcher: ['/login', '/register', '/forgot-password', '/reset-password', '/admin/:path*', '/super-admin', '/super-admin/login'],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineMigration, type MigrationContext } from '@holo-js/db'

export default defineMigration({
async up({ schema }: MigrationContext) {
await schema.createTable('admins', (table) => {
table.id()
table.string('name')
table.string('email').unique()
table.string('password').nullable()
table.string('avatar').nullable()
table.timestamp('email_verified_at').nullable()
table.timestamps()
})
},
async down({ schema }: MigrationContext) {
await schema.dropTable('admins')
},
})
Loading