@liedsonc/core-auth-kit 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +759 -0
- package/auth-ui/components/auth-card.tsx +49 -0
- package/auth-ui/components/auth-form.tsx +53 -0
- package/auth-ui/components/error-message.tsx +24 -0
- package/auth-ui/components/form-field.tsx +39 -0
- package/auth-ui/components/index.ts +8 -0
- package/auth-ui/components/loading-spinner.tsx +19 -0
- package/auth-ui/components/oauth-buttons.tsx +49 -0
- package/auth-ui/components/password-input.tsx +93 -0
- package/auth-ui/components/success-message.tsx +24 -0
- package/auth-ui/components/ui/button.tsx +52 -0
- package/auth-ui/components/ui/card.tsx +75 -0
- package/auth-ui/components/ui/input.tsx +21 -0
- package/auth-ui/components/ui/label.tsx +25 -0
- package/auth-ui/context.tsx +26 -0
- package/auth-ui/hooks/use-auth.ts +131 -0
- package/auth-ui/hooks/use-oauth.ts +25 -0
- package/auth-ui/index.ts +28 -0
- package/auth-ui/package.json +55 -0
- package/auth-ui/pages/forgot-password/index.tsx +83 -0
- package/auth-ui/pages/index.ts +5 -0
- package/auth-ui/pages/login/index.tsx +119 -0
- package/auth-ui/pages/register/index.tsx +149 -0
- package/auth-ui/pages/reset-password/index.tsx +133 -0
- package/auth-ui/pages/verify-email/index.tsx +143 -0
- package/auth-ui/styles/index.css +7 -0
- package/auth-ui/tsconfig.json +33 -0
- package/auth-ui/types/index.ts +34 -0
- package/auth-ui/utils.ts +6 -0
- package/package.json +32 -0
- package/postcss.config.mjs +9 -0
- package/tailwind.config.ts +41 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useAuth } from "../../hooks/use-auth";
|
|
5
|
+
import { useSearchParams } from "next/navigation";
|
|
6
|
+
import { AuthCard } from "../../components/auth-card";
|
|
7
|
+
import { AuthForm } from "../../components/auth-form";
|
|
8
|
+
import { FormField } from "../../components/form-field";
|
|
9
|
+
import { PasswordInput } from "../../components/password-input";
|
|
10
|
+
import { Button } from "../../components/ui/button";
|
|
11
|
+
import { useState, Suspense } from "react";
|
|
12
|
+
|
|
13
|
+
const genericError = "Something went wrong. Please request a new reset link.";
|
|
14
|
+
|
|
15
|
+
function ResetPasswordForm() {
|
|
16
|
+
const searchParams = useSearchParams();
|
|
17
|
+
const token = searchParams.get("token") ?? "";
|
|
18
|
+
const { resetPassword, loading, error, clearError } = useAuth();
|
|
19
|
+
const [password, setPassword] = useState("");
|
|
20
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
21
|
+
const [fieldErrors, setFieldErrors] = useState<{ password?: string; confirm?: string }>({});
|
|
22
|
+
|
|
23
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
clearError();
|
|
26
|
+
setFieldErrors({});
|
|
27
|
+
const err: { password?: string; confirm?: string } = {};
|
|
28
|
+
if (!token) err.password = "Invalid or missing reset link.";
|
|
29
|
+
if (!password) err.password = "Password is required.";
|
|
30
|
+
if (password.length < 8) err.password = "Password must be at least 8 characters.";
|
|
31
|
+
if (password !== confirmPassword) err.confirm = "Passwords do not match.";
|
|
32
|
+
if (Object.keys(err).length) {
|
|
33
|
+
setFieldErrors(err);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const result = await resetPassword(token, password);
|
|
37
|
+
if (!result.success) setFieldErrors({ password: genericError });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (!token) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
43
|
+
<AuthCard
|
|
44
|
+
title="Reset password"
|
|
45
|
+
subtitle="Invalid or expired link"
|
|
46
|
+
footer={
|
|
47
|
+
<Link
|
|
48
|
+
href="/forgot-password"
|
|
49
|
+
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
|
50
|
+
>
|
|
51
|
+
Request a new reset link
|
|
52
|
+
</Link>
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
<p className="text-sm text-muted-foreground">
|
|
56
|
+
This reset link is invalid or has expired. Please request a new one.
|
|
57
|
+
</p>
|
|
58
|
+
</AuthCard>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
65
|
+
<AuthCard
|
|
66
|
+
title="Reset password"
|
|
67
|
+
subtitle="Enter your new password"
|
|
68
|
+
footer={
|
|
69
|
+
<div className="flex w-full justify-center text-sm text-muted-foreground">
|
|
70
|
+
<Link
|
|
71
|
+
href="/login"
|
|
72
|
+
className="underline underline-offset-4 hover:text-foreground"
|
|
73
|
+
>
|
|
74
|
+
Back to sign in
|
|
75
|
+
</Link>
|
|
76
|
+
</div>
|
|
77
|
+
}
|
|
78
|
+
>
|
|
79
|
+
<AuthForm onSubmit={handleSubmit} loading={loading} error={error ? genericError : undefined}>
|
|
80
|
+
<FormField label="New password" htmlFor="reset-password" error={fieldErrors.password} required>
|
|
81
|
+
<PasswordInput
|
|
82
|
+
id="reset-password"
|
|
83
|
+
placeholder="••••••••"
|
|
84
|
+
autoComplete="new-password"
|
|
85
|
+
value={password}
|
|
86
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
87
|
+
disabled={loading}
|
|
88
|
+
showStrength
|
|
89
|
+
aria-invalid={!!fieldErrors.password}
|
|
90
|
+
/>
|
|
91
|
+
</FormField>
|
|
92
|
+
<FormField
|
|
93
|
+
label="Confirm password"
|
|
94
|
+
htmlFor="reset-confirm"
|
|
95
|
+
error={fieldErrors.confirm}
|
|
96
|
+
required
|
|
97
|
+
>
|
|
98
|
+
<PasswordInput
|
|
99
|
+
id="reset-confirm"
|
|
100
|
+
placeholder="••••••••"
|
|
101
|
+
autoComplete="new-password"
|
|
102
|
+
value={confirmPassword}
|
|
103
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
104
|
+
disabled={loading}
|
|
105
|
+
aria-invalid={!!fieldErrors.confirm}
|
|
106
|
+
/>
|
|
107
|
+
</FormField>
|
|
108
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
109
|
+
{loading ? (
|
|
110
|
+
<span className="inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
111
|
+
) : (
|
|
112
|
+
"Reset password"
|
|
113
|
+
)}
|
|
114
|
+
</Button>
|
|
115
|
+
</AuthForm>
|
|
116
|
+
</AuthCard>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function ResetPasswordPage() {
|
|
122
|
+
return (
|
|
123
|
+
<Suspense
|
|
124
|
+
fallback={
|
|
125
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
126
|
+
<span className="inline-block size-8 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
127
|
+
</div>
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<ResetPasswordForm />
|
|
131
|
+
</Suspense>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useAuth } from "../../hooks/use-auth";
|
|
5
|
+
import { useSearchParams } from "next/navigation";
|
|
6
|
+
import { AuthCard } from "../../components/auth-card";
|
|
7
|
+
import { SuccessMessage } from "../../components/success-message";
|
|
8
|
+
import { ErrorMessage } from "../../components/error-message";
|
|
9
|
+
import { Button } from "../../components/ui/button";
|
|
10
|
+
import { useEffect, useState, Suspense } from "react";
|
|
11
|
+
|
|
12
|
+
type VerifyState = "idle" | "loading" | "success" | "expired" | "invalid";
|
|
13
|
+
|
|
14
|
+
function VerifyEmailContent() {
|
|
15
|
+
const searchParams = useSearchParams();
|
|
16
|
+
const token = searchParams.get("token") ?? "";
|
|
17
|
+
const { verifyEmail, loading } = useAuth();
|
|
18
|
+
const [state, setState] = useState<VerifyState>("idle");
|
|
19
|
+
const [checked, setChecked] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!token || checked) return;
|
|
23
|
+
setChecked(true);
|
|
24
|
+
setState("loading");
|
|
25
|
+
verifyEmail(token).then((result) => {
|
|
26
|
+
if (result.success) setState("success");
|
|
27
|
+
else if (result.error.code === "EXPIRED" || result.error.code === "TOKEN_EXPIRED")
|
|
28
|
+
setState("expired");
|
|
29
|
+
else setState("invalid");
|
|
30
|
+
});
|
|
31
|
+
}, [token, checked, verifyEmail]);
|
|
32
|
+
|
|
33
|
+
if (!token) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
36
|
+
<AuthCard
|
|
37
|
+
title="Verify email"
|
|
38
|
+
subtitle="Invalid verification link"
|
|
39
|
+
footer={
|
|
40
|
+
<Link
|
|
41
|
+
href="/login"
|
|
42
|
+
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
|
43
|
+
>
|
|
44
|
+
Go to sign in
|
|
45
|
+
</Link>
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
<ErrorMessage>This verification link is invalid.</ErrorMessage>
|
|
49
|
+
</AuthCard>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (state === "loading" || (state === "idle" && loading)) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
57
|
+
<AuthCard title="Verify email" subtitle="Verifying your email...">
|
|
58
|
+
<div className="flex justify-center py-4">
|
|
59
|
+
<span
|
|
60
|
+
role="status"
|
|
61
|
+
aria-label="Verifying"
|
|
62
|
+
className="inline-block size-8 animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
</AuthCard>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (state === "success") {
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
73
|
+
<AuthCard
|
|
74
|
+
title="Email verified"
|
|
75
|
+
subtitle="Your account is ready"
|
|
76
|
+
footer={
|
|
77
|
+
<Button asChild className="w-full">
|
|
78
|
+
<Link href="/login">Sign in</Link>
|
|
79
|
+
</Button>
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
<SuccessMessage>Your email has been verified. You can now sign in.</SuccessMessage>
|
|
83
|
+
</AuthCard>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (state === "expired") {
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
91
|
+
<AuthCard
|
|
92
|
+
title="Link expired"
|
|
93
|
+
subtitle="This verification link has expired"
|
|
94
|
+
footer={
|
|
95
|
+
<Link
|
|
96
|
+
href="/login"
|
|
97
|
+
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
|
98
|
+
>
|
|
99
|
+
Go to sign in
|
|
100
|
+
</Link>
|
|
101
|
+
}
|
|
102
|
+
>
|
|
103
|
+
<ErrorMessage>
|
|
104
|
+
This verification link has expired. Please sign in to request a new one.
|
|
105
|
+
</ErrorMessage>
|
|
106
|
+
</AuthCard>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
113
|
+
<AuthCard
|
|
114
|
+
title="Verification failed"
|
|
115
|
+
subtitle="We couldn't verify your email"
|
|
116
|
+
footer={
|
|
117
|
+
<Link
|
|
118
|
+
href="/login"
|
|
119
|
+
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
|
120
|
+
>
|
|
121
|
+
Go to sign in
|
|
122
|
+
</Link>
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<ErrorMessage>This verification link is invalid or has already been used.</ErrorMessage>
|
|
126
|
+
</AuthCard>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function VerifyEmailPage() {
|
|
132
|
+
return (
|
|
133
|
+
<Suspense
|
|
134
|
+
fallback={
|
|
135
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
136
|
+
<span className="inline-block size-8 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
137
|
+
</div>
|
|
138
|
+
}
|
|
139
|
+
>
|
|
140
|
+
<VerifyEmailContent />
|
|
141
|
+
</Suspense>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"incremental": true,
|
|
17
|
+
"plugins": [
|
|
18
|
+
{
|
|
19
|
+
"name": "next"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/*": ["./*"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"include": [
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx"
|
|
29
|
+
],
|
|
30
|
+
"exclude": [
|
|
31
|
+
"node_modules"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type OAuthProvider = "google" | "apple";
|
|
4
|
+
|
|
5
|
+
export interface AuthError {
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AuthSession {
|
|
11
|
+
user: { id: string; email?: string };
|
|
12
|
+
expiresAt?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AuthClient {
|
|
16
|
+
login: (email: string, password: string) => Promise<{ success: true } | { success: false; error: AuthError }>;
|
|
17
|
+
register: (email: string, password: string) => Promise<{ success: true } | { success: false; error: AuthError }>;
|
|
18
|
+
logout: () => Promise<void>;
|
|
19
|
+
forgotPassword: (email: string) => Promise<{ success: true } | { success: false; error: AuthError }>;
|
|
20
|
+
resetPassword: (token: string, newPassword: string) => Promise<{ success: true } | { success: false; error: AuthError }>;
|
|
21
|
+
verifyEmail: (token: string) => Promise<{ success: true } | { success: false; error: AuthError }>;
|
|
22
|
+
getSession?: () => Promise<AuthSession | null>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type AuthFormState = "idle" | "loading" | "success" | "error";
|
|
26
|
+
|
|
27
|
+
export interface AuthUIConfig {
|
|
28
|
+
authClient: AuthClient;
|
|
29
|
+
oauthProviders?: { provider: OAuthProvider; enabled: boolean }[];
|
|
30
|
+
logo?: ReactNode;
|
|
31
|
+
redirectAfterLogin?: string;
|
|
32
|
+
redirectAfterRegister?: string;
|
|
33
|
+
redirectAfterReset?: string;
|
|
34
|
+
}
|
package/auth-ui/utils.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liedsonc/core-auth-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Production-ready authentication UI package for Next.js App Router",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "cd auth-ui && npm run build",
|
|
7
|
+
"prepublishOnly": "npm run build"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"nextjs",
|
|
11
|
+
"auth",
|
|
12
|
+
"authentication",
|
|
13
|
+
"ui",
|
|
14
|
+
"shadcn",
|
|
15
|
+
"react",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"workspaces": [
|
|
21
|
+
"auth-ui"
|
|
22
|
+
],
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"@types/react": "^19.0.0",
|
|
26
|
+
"@types/react-dom": "^19.0.0",
|
|
27
|
+
"autoprefixer": "^10.4.20",
|
|
28
|
+
"postcss": "^8.4.47",
|
|
29
|
+
"tailwindcss": "^3.4.14",
|
|
30
|
+
"typescript": "^5.6.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
darkMode: "class",
|
|
5
|
+
content: [
|
|
6
|
+
"./auth-ui/**/*.{js,ts,jsx,tsx,mdx}",
|
|
7
|
+
],
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
colors: {
|
|
11
|
+
background: "hsl(var(--background))",
|
|
12
|
+
foreground: "hsl(var(--foreground))",
|
|
13
|
+
card: "hsl(var(--card))",
|
|
14
|
+
"card-foreground": "hsl(var(--card-foreground))",
|
|
15
|
+
popover: "hsl(var(--popover))",
|
|
16
|
+
"popover-foreground": "hsl(var(--popover-foreground))",
|
|
17
|
+
primary: "hsl(var(--primary))",
|
|
18
|
+
"primary-foreground": "hsl(var(--primary-foreground))",
|
|
19
|
+
secondary: "hsl(var(--secondary))",
|
|
20
|
+
"secondary-foreground": "hsl(var(--secondary-foreground))",
|
|
21
|
+
muted: "hsl(var(--muted))",
|
|
22
|
+
"muted-foreground": "hsl(var(--muted-foreground))",
|
|
23
|
+
accent: "hsl(var(--accent))",
|
|
24
|
+
"accent-foreground": "hsl(var(--accent-foreground))",
|
|
25
|
+
destructive: "hsl(var(--destructive))",
|
|
26
|
+
"destructive-foreground": "hsl(var(--destructive-foreground))",
|
|
27
|
+
border: "hsl(var(--border))",
|
|
28
|
+
input: "hsl(var(--input))",
|
|
29
|
+
ring: "hsl(var(--ring))",
|
|
30
|
+
},
|
|
31
|
+
borderRadius: {
|
|
32
|
+
lg: "var(--radius)",
|
|
33
|
+
md: "calc(var(--radius) - 2px)",
|
|
34
|
+
sm: "calc(var(--radius) - 4px)",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
plugins: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default config;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"incremental": true
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"auth-ui/**/*.ts",
|
|
20
|
+
"auth-ui/**/*.tsx"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules"
|
|
24
|
+
]
|
|
25
|
+
}
|