@alinsafawi/aegis-auth 0.2.3 → 0.2.4

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