@alinsafawi/aegis-auth 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +442 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23616,23 +23616,27 @@ export default function LoginPage() {
23616
23616
  setError('')
23617
23617
  setLoading(true)
23618
23618
 
23619
- const csrf = document.cookie
23620
- .split('; ')
23621
- .find((r) => r.startsWith('${appName.toLowerCase().replace(/[^a-z]/g, "")}_csrf='))
23622
- ?.split('=')[1]
23623
-
23624
- const res = await fetch('/api/auth/login', {
23625
- method: 'POST',
23626
- headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf ?? '' },
23627
- body: JSON.stringify({ identifier, password }),
23628
- })
23619
+ try {
23620
+ const csrf = document.cookie
23621
+ .split('; ')
23622
+ .find((r) => r.startsWith('${appName.toLowerCase().replace(/[^a-z]/g, "")}_csrf='))
23623
+ ?.split('=')[1]
23629
23624
 
23630
- const data = await res.json()
23631
- setLoading(false)
23625
+ const res = await fetch('/api/auth/login', {
23626
+ method: 'POST',
23627
+ headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf ?? '' },
23628
+ body: JSON.stringify({ identifier, password }),
23629
+ })
23632
23630
 
23633
- if (!res.ok) { setError(data.error ?? 'Login failed'); return }
23634
- if (data.requiresTwoFactor) { router.push('/two-factor'); return }
23635
- router.push(\`/\${data.role}/dashboard\`)
23631
+ const data = await res.json()
23632
+ if (!res.ok) { setError(data.error ?? 'Login failed'); return }
23633
+ if (data.requiresTwoFactor) { router.push('/two-factor'); return }
23634
+ router.push(\`/\${data.role}/dashboard\`)
23635
+ } catch {
23636
+ setError('Network error \u2014 please try again.')
23637
+ } finally {
23638
+ setLoading(false)
23639
+ }
23636
23640
  }
23637
23641
 
23638
23642
  return (
@@ -23747,6 +23751,402 @@ function generatePackageJson(name, packageManager) {
23747
23751
  2
23748
23752
  );
23749
23753
  }
23754
+ function generatePrismaLib() {
23755
+ return `import { PrismaClient } from '@prisma/client'
23756
+
23757
+ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
23758
+
23759
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient()
23760
+
23761
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
23762
+ `;
23763
+ }
23764
+ function generateLoginRoute() {
23765
+ return `import { createLoginHandler } from '@alinsafawi/aegis-auth-next'
23766
+ import { prisma } from '@/lib/prisma'
23767
+ import config from '../../../../../auth.config'
23768
+
23769
+ export const POST = createLoginHandler(config, prisma)
23770
+ `;
23771
+ }
23772
+ function generateLogoutRoute() {
23773
+ return `import { createLogoutHandler } from '@alinsafawi/aegis-auth-next'
23774
+ import config from '../../../../../auth.config'
23775
+
23776
+ export const POST = createLogoutHandler(config)
23777
+ `;
23778
+ }
23779
+ function generateTwoFactorRoute() {
23780
+ return `import { createTwoFactorHandler } from '@alinsafawi/aegis-auth-next'
23781
+ import { prisma } from '@/lib/prisma'
23782
+ import config from '../../../../../auth.config'
23783
+
23784
+ export const POST = createTwoFactorHandler(config, prisma)
23785
+ `;
23786
+ }
23787
+ function generateChangePasswordRoute() {
23788
+ return `import { createChangePasswordHandler } from '@alinsafawi/aegis-auth-next'
23789
+ import { prisma } from '@/lib/prisma'
23790
+ import config from '../../../../../auth.config'
23791
+
23792
+ export const POST = createChangePasswordHandler(config, prisma)
23793
+ `;
23794
+ }
23795
+ function generateForgotPasswordPage(language) {
23796
+ const ts = language === "typescript";
23797
+ return `'use client'
23798
+
23799
+ import { useState } from 'react'
23800
+
23801
+ export default function ForgotPasswordPage() {
23802
+ const [email, setEmail] = useState('')
23803
+ const [sent, setSent] = useState(false)
23804
+ const [error, setError] = useState('')
23805
+ const [loading, setLoading] = useState(false)
23806
+
23807
+ async function handleSubmit(e${ts ? ": React.FormEvent" : ""}) {
23808
+ e.preventDefault()
23809
+ setLoading(true)
23810
+ setError('')
23811
+ const res = await fetch('/api/auth/forgot-password', {
23812
+ method: 'POST',
23813
+ headers: { 'Content-Type': 'application/json' },
23814
+ body: JSON.stringify({ email }),
23815
+ })
23816
+ setLoading(false)
23817
+ if (!res.ok) { setError((await res.json()).error ?? 'Something went wrong'); return }
23818
+ setSent(true)
23819
+ }
23820
+
23821
+ if (sent) {
23822
+ return (
23823
+ <main className="min-h-screen flex items-center justify-center p-4">
23824
+ <div className="w-full max-w-sm text-center space-y-3">
23825
+ <h1 className="text-2xl font-bold">Check your email</h1>
23826
+ <p className="text-[var(--muted)] text-sm">
23827
+ If an account exists for <strong>{email}</strong>, a reset link has been sent.
23828
+ </p>
23829
+ <a href="/login" className="block text-sm text-[var(--muted)] hover:underline mt-4">
23830
+ Back to login
23831
+ </a>
23832
+ </div>
23833
+ </main>
23834
+ )
23835
+ }
23836
+
23837
+ return (
23838
+ <main className="min-h-screen flex items-center justify-center p-4">
23839
+ <div className="w-full max-w-sm">
23840
+ <div className="text-center mb-8">
23841
+ <h1 className="text-2xl font-bold">Forgot password</h1>
23842
+ <p className="text-[var(--muted)] text-sm mt-1">We'll send you a reset link.</p>
23843
+ </div>
23844
+ <form onSubmit={handleSubmit} className="space-y-4">
23845
+ {error && <div className="text-sm text-red-500 bg-red-50 border border-red-200 rounded-md p-3">{error}</div>}
23846
+ <div>
23847
+ <label className="block text-sm font-medium mb-1">Email address</label>
23848
+ <input
23849
+ type="email"
23850
+ value={email}
23851
+ onChange={(e) => setEmail(e.target.value)}
23852
+ required
23853
+ autoComplete="email"
23854
+ className="w-full px-3 py-2 rounded-md border bg-[var(--input)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
23855
+ />
23856
+ </div>
23857
+ <button
23858
+ type="submit"
23859
+ disabled={loading}
23860
+ style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
23861
+ className="w-full py-2 px-4 rounded-md font-medium disabled:opacity-50 transition-opacity"
23862
+ >
23863
+ {loading ? 'Sending\u2026' : 'Send reset link'}
23864
+ </button>
23865
+ <div className="text-center">
23866
+ <a href="/login" className="text-sm text-[var(--muted)] hover:underline">Back to login</a>
23867
+ </div>
23868
+ </form>
23869
+ </div>
23870
+ </main>
23871
+ )
23872
+ }
23873
+ `;
23874
+ }
23875
+ function generateResetPasswordPage(language) {
23876
+ const ts = language === "typescript";
23877
+ return `'use client'
23878
+
23879
+ import { useState } from 'react'
23880
+ import { useSearchParams, useRouter } from 'next/navigation'
23881
+
23882
+ export default function ResetPasswordPage() {
23883
+ const params = useSearchParams()
23884
+ const router = useRouter()
23885
+ const token = params.get('token') ?? ''
23886
+ const [password, setPassword] = useState('')
23887
+ const [confirm, setConfirm] = useState('')
23888
+ const [error, setError] = useState('')
23889
+ const [loading, setLoading] = useState(false)
23890
+
23891
+ async function handleSubmit(e${ts ? ": React.FormEvent" : ""}) {
23892
+ e.preventDefault()
23893
+ if (password !== confirm) { setError('Passwords do not match'); return }
23894
+ setLoading(true)
23895
+ setError('')
23896
+ const res = await fetch('/api/auth/reset-password', {
23897
+ method: 'POST',
23898
+ headers: { 'Content-Type': 'application/json' },
23899
+ body: JSON.stringify({ token, newPassword: password }),
23900
+ })
23901
+ setLoading(false)
23902
+ if (!res.ok) { setError((await res.json()).error ?? 'Something went wrong'); return }
23903
+ router.push('/login?reset=1')
23904
+ }
23905
+
23906
+ return (
23907
+ <main className="min-h-screen flex items-center justify-center p-4">
23908
+ <div className="w-full max-w-sm">
23909
+ <div className="text-center mb-8">
23910
+ <h1 className="text-2xl font-bold">Reset password</h1>
23911
+ </div>
23912
+ <form onSubmit={handleSubmit} className="space-y-4">
23913
+ {error && <div className="text-sm text-red-500 bg-red-50 border border-red-200 rounded-md p-3">{error}</div>}
23914
+ <div>
23915
+ <label className="block text-sm font-medium mb-1">New password</label>
23916
+ <input
23917
+ type="password"
23918
+ value={password}
23919
+ onChange={(e) => setPassword(e.target.value)}
23920
+ required
23921
+ autoComplete="new-password"
23922
+ className="w-full px-3 py-2 rounded-md border bg-[var(--input)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
23923
+ />
23924
+ </div>
23925
+ <div>
23926
+ <label className="block text-sm font-medium mb-1">Confirm new password</label>
23927
+ <input
23928
+ type="password"
23929
+ value={confirm}
23930
+ onChange={(e) => setConfirm(e.target.value)}
23931
+ required
23932
+ autoComplete="new-password"
23933
+ className="w-full px-3 py-2 rounded-md border bg-[var(--input)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
23934
+ />
23935
+ </div>
23936
+ <button
23937
+ type="submit"
23938
+ disabled={loading}
23939
+ style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
23940
+ className="w-full py-2 px-4 rounded-md font-medium disabled:opacity-50 transition-opacity"
23941
+ >
23942
+ {loading ? 'Resetting\u2026' : 'Reset password'}
23943
+ </button>
23944
+ </form>
23945
+ </div>
23946
+ </main>
23947
+ )
23948
+ }
23949
+ `;
23950
+ }
23951
+ function generateTwoFactorPage(language) {
23952
+ const ts = language === "typescript";
23953
+ return `'use client'
23954
+
23955
+ import { useState } from 'react'
23956
+ import { useRouter } from 'next/navigation'
23957
+
23958
+ export default function TwoFactorPage() {
23959
+ const [code, setCode] = useState('')
23960
+ const [error, setError] = useState('')
23961
+ const [loading, setLoading] = useState(false)
23962
+ const router = useRouter()
23963
+
23964
+ async function handleSubmit(e${ts ? ": React.FormEvent" : ""}) {
23965
+ e.preventDefault()
23966
+ setLoading(true)
23967
+ setError('')
23968
+ const res = await fetch('/api/auth/two-factor', {
23969
+ method: 'POST',
23970
+ headers: { 'Content-Type': 'application/json' },
23971
+ body: JSON.stringify({ code }),
23972
+ })
23973
+ const data = await res.json()
23974
+ setLoading(false)
23975
+ if (!res.ok) { setError(data.error ?? 'Invalid code'); return }
23976
+ router.push(\`/\${data.role}/dashboard\`)
23977
+ }
23978
+
23979
+ return (
23980
+ <main className="min-h-screen flex items-center justify-center p-4">
23981
+ <div className="w-full max-w-sm">
23982
+ <div className="text-center mb-8">
23983
+ <h1 className="text-2xl font-bold">Two-factor authentication</h1>
23984
+ <p className="text-[var(--muted)] text-sm mt-1">Enter the 6-digit code from your authenticator app.</p>
23985
+ </div>
23986
+ <form onSubmit={handleSubmit} className="space-y-4">
23987
+ {error && <div className="text-sm text-red-500 bg-red-50 border border-red-200 rounded-md p-3">{error}</div>}
23988
+ <div>
23989
+ <label className="block text-sm font-medium mb-1">Authentication code</label>
23990
+ <input
23991
+ type="text"
23992
+ inputMode="numeric"
23993
+ pattern="[0-9]*"
23994
+ maxLength={6}
23995
+ value={code}
23996
+ onChange={(e) => setCode(e.target.value.replace(/D/g, ''))}
23997
+ required
23998
+ autoComplete="one-time-code"
23999
+ placeholder="000000"
24000
+ className="w-full px-3 py-2 rounded-md border bg-[var(--input)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] text-center tracking-widest text-lg"
24001
+ />
24002
+ </div>
24003
+ <button
24004
+ type="submit"
24005
+ disabled={loading || code.length !== 6}
24006
+ style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
24007
+ className="w-full py-2 px-4 rounded-md font-medium disabled:opacity-50 transition-opacity"
24008
+ >
24009
+ {loading ? 'Verifying\u2026' : 'Verify'}
24010
+ </button>
24011
+ <div className="text-center">
24012
+ <a href="/login" className="text-sm text-[var(--muted)] hover:underline">Back to login</a>
24013
+ </div>
24014
+ </form>
24015
+ </div>
24016
+ </main>
24017
+ )
24018
+ }
24019
+ `;
24020
+ }
24021
+ function generateVerifyEmailNoticePage() {
24022
+ return `export default function VerifyEmailNoticePage() {
24023
+ return (
24024
+ <main className="min-h-screen flex items-center justify-center p-4">
24025
+ <div className="w-full max-w-sm text-center space-y-3">
24026
+ <h1 className="text-2xl font-bold">Verify your email</h1>
24027
+ <p className="text-[var(--muted)] text-sm">
24028
+ We sent a verification code to your email address. Enter it below to activate your account.
24029
+ </p>
24030
+ <a href="/verify-email" className="block mt-4 text-sm underline" style={{ color: 'var(--primary)' }}>
24031
+ Enter verification code \u2192
24032
+ </a>
24033
+ </div>
24034
+ </main>
24035
+ )
24036
+ }
24037
+ `;
24038
+ }
24039
+ function generateVerifyEmailPage(language) {
24040
+ const ts = language === "typescript";
24041
+ return `'use client'
24042
+
24043
+ import { useState } from 'react'
24044
+ import { useRouter } from 'next/navigation'
24045
+
24046
+ export default function VerifyEmailPage() {
24047
+ const [code, setCode] = useState('')
24048
+ const [error, setError] = useState('')
24049
+ const [loading, setLoading] = useState(false)
24050
+ const router = useRouter()
24051
+
24052
+ async function handleSubmit(e${ts ? ": React.FormEvent" : ""}) {
24053
+ e.preventDefault()
24054
+ setLoading(true)
24055
+ setError('')
24056
+ const res = await fetch('/api/auth/verify-email', {
24057
+ method: 'POST',
24058
+ headers: { 'Content-Type': 'application/json' },
24059
+ body: JSON.stringify({ code }),
24060
+ })
24061
+ const data = await res.json()
24062
+ setLoading(false)
24063
+ if (!res.ok) { setError(data.error ?? 'Invalid code'); return }
24064
+ router.push(\`/\${data.role}/dashboard\`)
24065
+ }
24066
+
24067
+ return (
24068
+ <main className="min-h-screen flex items-center justify-center p-4">
24069
+ <div className="w-full max-w-sm">
24070
+ <div className="text-center mb-8">
24071
+ <h1 className="text-2xl font-bold">Enter verification code</h1>
24072
+ <p className="text-[var(--muted)] text-sm mt-1">Check your email for the 6-digit code.</p>
24073
+ </div>
24074
+ <form onSubmit={handleSubmit} className="space-y-4">
24075
+ {error && <div className="text-sm text-red-500 bg-red-50 border border-red-200 rounded-md p-3">{error}</div>}
24076
+ <div>
24077
+ <label className="block text-sm font-medium mb-1">Verification code</label>
24078
+ <input
24079
+ type="text"
24080
+ inputMode="numeric"
24081
+ pattern="[0-9]*"
24082
+ maxLength={6}
24083
+ value={code}
24084
+ onChange={(e) => setCode(e.target.value.replace(/D/g, ''))}
24085
+ required
24086
+ autoComplete="one-time-code"
24087
+ placeholder="000000"
24088
+ className="w-full px-3 py-2 rounded-md border bg-[var(--input)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] text-center tracking-widest text-lg"
24089
+ />
24090
+ </div>
24091
+ <button
24092
+ type="submit"
24093
+ disabled={loading || code.length !== 6}
24094
+ style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
24095
+ className="w-full py-2 px-4 rounded-md font-medium disabled:opacity-50 transition-opacity"
24096
+ >
24097
+ {loading ? 'Verifying\u2026' : 'Verify email'}
24098
+ </button>
24099
+ <div className="text-center">
24100
+ <a href="/login" className="text-sm text-[var(--muted)] hover:underline">Back to login</a>
24101
+ </div>
24102
+ </form>
24103
+ </div>
24104
+ </main>
24105
+ )
24106
+ }
24107
+ `;
24108
+ }
24109
+ function generateForgotPasswordRoute(language) {
24110
+ const ts = language === "typescript";
24111
+ return `import { NextRequest, NextResponse } from 'next/server'
24112
+ import { prisma } from '@/lib/prisma'
24113
+ import config from '../../../../../auth.config'
24114
+
24115
+ export async function POST(request${ts ? ": NextRequest" : ""}) {
24116
+ // TODO: implement \u2014 find user by email, generate reset token, send email
24117
+ // You can use: import { sendEmail } from '@alinsafawi/aegis-auth-core'
24118
+ const { email } = await request.json()
24119
+ return NextResponse.json({ success: true })
24120
+ }
24121
+ `;
24122
+ }
24123
+ function generateResetPasswordRoute(language) {
24124
+ const ts = language === "typescript";
24125
+ return `import { NextRequest, NextResponse } from 'next/server'
24126
+ import { prisma } from '@/lib/prisma'
24127
+ import { hashPassword } from '@alinsafawi/aegis-auth-core'
24128
+ import config from '../../../../../auth.config'
24129
+
24130
+ export async function POST(request${ts ? ": NextRequest" : ""}) {
24131
+ // TODO: implement \u2014 verify token, update password hash
24132
+ const { token, newPassword } = await request.json()
24133
+ return NextResponse.json({ success: true })
24134
+ }
24135
+ `;
24136
+ }
24137
+ function generateVerifyEmailRoute(language) {
24138
+ const ts = language === "typescript";
24139
+ return `import { NextRequest, NextResponse } from 'next/server'
24140
+ import { prisma } from '@/lib/prisma'
24141
+ import config from '../../../../../auth.config'
24142
+
24143
+ export async function POST(request${ts ? ": NextRequest" : ""}) {
24144
+ // TODO: implement \u2014 verify code, mark emailVerified = true, return role
24145
+ const { code } = await request.json()
24146
+ return NextResponse.json({ success: true, role: 'user' })
24147
+ }
24148
+ `;
24149
+ }
23750
24150
 
23751
24151
  // src/commands/init.ts
23752
24152
  var DEFAULT_COLOR = "oklch(0.6 0.2 240)";
@@ -23958,9 +24358,35 @@ dist
23958
24358
  await step("Creating session helpers", async () => {
23959
24359
  await writeFile(`src/lib/auth.${language === "typescript" ? "ts" : "js"}`, generateAuthLib(language));
23960
24360
  });
23961
- await step("Scaffolding auth pages", async () => {
24361
+ await step("Scaffolding auth pages and API routes", async () => {
23962
24362
  const e = language === "typescript" ? "tsx" : "jsx";
24363
+ const r = language === "typescript" ? "ts" : "js";
24364
+ await writeFile(`src/lib/prisma.${r}`, generatePrismaLib());
24365
+ await writeFile(`src/app/api/auth/login/route.${r}`, generateLoginRoute());
24366
+ await writeFile(`src/app/api/auth/logout/route.${r}`, generateLogoutRoute());
24367
+ await writeFile(`src/app/api/auth/change-password/route.${r}`, generateChangePasswordRoute());
24368
+ if (features.twoFactor) {
24369
+ await writeFile(`src/app/api/auth/two-factor/route.${r}`, generateTwoFactorRoute());
24370
+ }
24371
+ if (features.passwordReset) {
24372
+ await writeFile(`src/app/api/auth/forgot-password/route.${r}`, generateForgotPasswordRoute(language));
24373
+ await writeFile(`src/app/api/auth/reset-password/route.${r}`, generateResetPasswordRoute(language));
24374
+ }
24375
+ if (features.emailVerification) {
24376
+ await writeFile(`src/app/api/auth/verify-email/route.${r}`, generateVerifyEmailRoute(language));
24377
+ }
23963
24378
  await writeFile(`src/app/(auth)/login/page.${e}`, generateLoginPage(app.appName, DEFAULT_COLOR, language));
24379
+ if (features.passwordReset) {
24380
+ await writeFile(`src/app/(auth)/forgot-password/page.${e}`, generateForgotPasswordPage(language));
24381
+ await writeFile(`src/app/(auth)/reset-password/page.${e}`, generateResetPasswordPage(language));
24382
+ }
24383
+ if (features.twoFactor) {
24384
+ await writeFile(`src/app/(auth)/two-factor/page.${e}`, generateTwoFactorPage(language));
24385
+ }
24386
+ if (features.emailVerification) {
24387
+ await writeFile(`src/app/(auth)/verify-email-notice/page.${e}`, generateVerifyEmailNoticePage());
24388
+ await writeFile(`src/app/(auth)/verify-email/page.${e}`, generateVerifyEmailPage(language));
24389
+ }
23964
24390
  });
23965
24391
  await step("Creating .env and .env.example", async () => {
23966
24392
  await writeFile(".env", generateEnvFile(app.cookiePrefix, infra, features));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alinsafawi/aegis-auth",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "The shield your Next.js app deserves — full-stack auth in minutes",
5
5
  "bin": {
6
6
  "aegis-auth": "dist/index.js"