nextworks 0.0.1 → 0.1.0-alpha.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/README.md +209 -30
- package/dist/.gitkeep +0 -0
- package/dist/cli_manifests/auth_manifest.json +86 -0
- package/dist/cli_manifests/blocks_manifest.json +185 -0
- package/dist/cli_manifests/data_manifest.json +51 -0
- package/dist/cli_manifests/forms_manifest.json +61 -0
- package/dist/commands/admin-posts.d.ts +2 -0
- package/dist/commands/admin-posts.d.ts.map +1 -0
- package/dist/commands/admin-posts.js +15 -0
- package/dist/commands/admin-posts.js.map +1 -0
- package/dist/commands/admin-users.d.ts +2 -0
- package/dist/commands/admin-users.d.ts.map +1 -0
- package/dist/commands/admin-users.js +15 -0
- package/dist/commands/admin-users.js.map +1 -0
- package/dist/commands/auth-core.d.ts +2 -0
- package/dist/commands/auth-core.d.ts.map +1 -0
- package/dist/commands/auth-core.js +83 -0
- package/dist/commands/auth-core.js.map +1 -0
- package/dist/commands/auth-forms.d.ts +2 -0
- package/dist/commands/auth-forms.d.ts.map +1 -0
- package/dist/commands/auth-forms.js +15 -0
- package/dist/commands/auth-forms.js.map +1 -0
- package/dist/commands/blocks-options.d.ts +7 -0
- package/dist/commands/blocks-options.d.ts.map +1 -0
- package/dist/commands/blocks-options.js +19 -0
- package/dist/commands/blocks-options.js.map +1 -0
- package/dist/commands/blocks.d.ts +7 -0
- package/dist/commands/blocks.d.ts.map +1 -0
- package/dist/commands/blocks.js +140 -0
- package/dist/commands/blocks.js.map +1 -0
- package/dist/commands/data.d.ts +3 -0
- package/dist/commands/data.d.ts.map +1 -0
- package/dist/commands/data.js +88 -0
- package/dist/commands/data.js.map +1 -0
- package/dist/commands/forms.d.ts +6 -0
- package/dist/commands/forms.d.ts.map +1 -0
- package/dist/commands/forms.js +107 -0
- package/dist/commands/forms.js.map +1 -0
- package/dist/commands/remove-auth-core.d.ts +2 -0
- package/dist/commands/remove-auth-core.d.ts.map +1 -0
- package/dist/commands/remove-auth-core.js +69 -0
- package/dist/commands/remove-auth-core.js.map +1 -0
- package/dist/commands/remove-blocks.d.ts +2 -0
- package/dist/commands/remove-blocks.d.ts.map +1 -0
- package/dist/commands/remove-blocks.js +36 -0
- package/dist/commands/remove-blocks.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/kits/auth-core/README.md +82 -0
- package/dist/kits/auth-core/app/(protected)/dashboard/page.tsx +8 -0
- package/dist/kits/auth-core/app/(protected)/layout.tsx +18 -0
- package/dist/kits/auth-core/app/(protected)/settings/profile/page.tsx +15 -0
- package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +114 -0
- package/dist/kits/auth-core/app/api/auth/[...nextauth]/route.ts +1 -0
- package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +114 -0
- package/dist/kits/auth-core/app/api/auth/providers/route.ts +6 -0
- package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +63 -0
- package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +6 -0
- package/dist/kits/auth-core/app/api/signup/route.ts +41 -0
- package/dist/kits/auth-core/app/auth/forgot-password/page.tsx +21 -0
- package/dist/kits/auth-core/app/auth/login/page.tsx +5 -0
- package/dist/kits/auth-core/app/auth/reset-password/page.tsx +187 -0
- package/dist/kits/auth-core/app/auth/signup/page.tsx +5 -0
- package/dist/kits/auth-core/app/auth/verify-email/page.tsx +11 -0
- package/dist/kits/auth-core/components/admin/admin-header.tsx +57 -0
- package/dist/kits/auth-core/components/auth/dashboard.tsx +237 -0
- package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -0
- package/dist/kits/auth-core/components/auth/login-form.tsx +467 -0
- package/dist/kits/auth-core/components/auth/logout-button.tsx +50 -0
- package/dist/kits/auth-core/components/auth/minimal-logout-button.tsx +40 -0
- package/dist/kits/auth-core/components/auth/signup-form.tsx +468 -0
- package/dist/kits/auth-core/components/require-auth.tsx +59 -0
- package/dist/kits/auth-core/components/session-provider.tsx +11 -0
- package/dist/kits/auth-core/components/ui/README.txt +1 -0
- package/dist/kits/auth-core/components/ui/button.tsx +55 -0
- package/dist/kits/auth-core/components/ui/input.tsx +25 -0
- package/dist/kits/auth-core/components/ui/label.tsx +23 -0
- package/dist/kits/auth-core/lib/api/errors.ts +14 -0
- package/dist/kits/auth-core/lib/auth-helpers.ts +29 -0
- package/dist/kits/auth-core/lib/auth.ts +142 -0
- package/dist/kits/auth-core/lib/email/dev-transport.ts +42 -0
- package/dist/kits/auth-core/lib/email/index.ts +28 -0
- package/dist/kits/auth-core/lib/email/provider-smtp.ts +36 -0
- package/dist/kits/auth-core/lib/forms/map-errors.ts +11 -0
- package/dist/kits/auth-core/lib/hash.ts +6 -0
- package/dist/kits/auth-core/lib/prisma.ts +15 -0
- package/dist/kits/auth-core/lib/server/result.ts +45 -0
- package/dist/kits/auth-core/lib/utils.ts +6 -0
- package/dist/kits/auth-core/lib/validation/forms.ts +88 -0
- package/dist/kits/auth-core/package-deps.json +19 -0
- package/dist/kits/auth-core/prisma/auth-models.prisma +81 -0
- package/dist/kits/auth-core/prisma/schema.prisma +81 -0
- package/dist/kits/auth-core/scripts/populate-tokenhash.mjs +26 -0
- package/dist/kits/auth-core/scripts/promote-admin.mjs +33 -0
- package/dist/kits/auth-core/scripts/seed-demo.mjs +40 -0
- package/dist/kits/auth-core/types/next-auth.d.ts +25 -0
- package/dist/kits/blocks/README.md +53 -0
- package/dist/kits/blocks/app/globals.css +175 -0
- package/dist/kits/blocks/app/templates/digitalagency/PresetThemeVars.tsx +80 -0
- package/dist/kits/blocks/app/templates/digitalagency/README.md +36 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/About.tsx +99 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/CTA.tsx +74 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Contact.tsx +227 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Footer.tsx +89 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Hero.tsx +90 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Navbar.tsx +168 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/NetworkPattern.tsx +297 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Portfolio.tsx +157 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Pricing.tsx +114 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Process.tsx +59 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Services.tsx +55 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Team.tsx +28 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Testimonials.tsx +65 -0
- package/dist/kits/blocks/app/templates/digitalagency/page.tsx +38 -0
- package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +85 -0
- package/dist/kits/blocks/app/templates/gallery/page.tsx +303 -0
- package/dist/kits/blocks/app/templates/productlaunch/PresetThemeVars.tsx +74 -0
- package/dist/kits/blocks/app/templates/productlaunch/README.md +55 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/About.tsx +178 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/CTA.tsx +93 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Contact.tsx +231 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/FAQ.tsx +93 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Features.tsx +84 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Footer.tsx +132 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Hero.tsx +89 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Navbar.tsx +162 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Pricing.tsx +106 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/ProcessTimeline.tsx +110 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/ServicesGrid.tsx +68 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Team.tsx +104 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Testimonials.tsx +89 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/TrustBadges.tsx +76 -0
- package/dist/kits/blocks/app/templates/productlaunch/page.tsx +45 -0
- package/dist/kits/blocks/app/templates/saasdashboard/PresetThemeVars.tsx +80 -0
- package/dist/kits/blocks/app/templates/saasdashboard/README.md +38 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Contact.tsx +176 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Dashboard.tsx +293 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/FAQ.tsx +55 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Features.tsx +91 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Footer.tsx +77 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Hero.tsx +105 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Hero_mask.tsx +127 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Navbar.tsx +159 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Pricing.tsx +90 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/SmoothScroll.tsx +97 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Testimonials.tsx +72 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/TrustBadges.tsx +53 -0
- package/dist/kits/blocks/app/templates/saasdashboard/page.tsx +39 -0
- package/dist/kits/blocks/components/app-providers.tsx +1 -0
- package/dist/kits/blocks/components/enhanced-theme-provider.tsx +195 -0
- package/dist/kits/blocks/components/sections/About.tsx +291 -0
- package/dist/kits/blocks/components/sections/CTA.tsx +258 -0
- package/dist/kits/blocks/components/sections/Contact.tsx +267 -0
- package/dist/kits/blocks/components/sections/FAQ.tsx +226 -0
- package/dist/kits/blocks/components/sections/Features.tsx +269 -0
- package/dist/kits/blocks/components/sections/Footer.tsx +302 -0
- package/dist/kits/blocks/components/sections/HeroMotion.tsx +307 -0
- package/dist/kits/blocks/components/sections/HeroOverlay.tsx +358 -0
- package/dist/kits/blocks/components/sections/HeroSplit.tsx +352 -0
- package/dist/kits/blocks/components/sections/Navbar.tsx +353 -0
- package/dist/kits/blocks/components/sections/Newsletter.tsx +156 -0
- package/dist/kits/blocks/components/sections/PortfolioSimple.tsx +550 -0
- package/dist/kits/blocks/components/sections/Pricing.tsx +264 -0
- package/dist/kits/blocks/components/sections/ProcessTimeline.tsx +325 -0
- package/dist/kits/blocks/components/sections/ServicesGrid.tsx +210 -0
- package/dist/kits/blocks/components/sections/Team.tsx +309 -0
- package/dist/kits/blocks/components/sections/Testimonials.tsx +158 -0
- package/dist/kits/blocks/components/sections/TrustBadges.tsx +162 -0
- package/dist/kits/blocks/components/theme-provider.tsx +34 -0
- package/dist/kits/blocks/components/ui/alert-dialog.tsx +134 -0
- package/dist/kits/blocks/components/ui/brand-node.tsx +121 -0
- package/dist/kits/blocks/components/ui/button.tsx +122 -0
- package/dist/kits/blocks/components/ui/button_bck.tsx +93 -0
- package/dist/kits/blocks/components/ui/card.tsx +95 -0
- package/dist/kits/blocks/components/ui/checkbox.tsx +30 -0
- package/dist/kits/blocks/components/ui/cta-button.tsx +125 -0
- package/dist/kits/blocks/components/ui/dropdown-menu.tsx +201 -0
- package/dist/kits/blocks/components/ui/feature-card.tsx +91 -0
- package/dist/kits/blocks/components/ui/input.tsx +27 -0
- package/dist/kits/blocks/components/ui/label.tsx +29 -0
- package/dist/kits/blocks/components/ui/pricing-card.tsx +120 -0
- package/dist/kits/blocks/components/ui/select.tsx +25 -0
- package/dist/kits/blocks/components/ui/skeleton.tsx +13 -0
- package/dist/kits/blocks/components/ui/switch.tsx +78 -0
- package/dist/kits/blocks/components/ui/table.tsx +98 -0
- package/dist/kits/blocks/components/ui/testimonial-card.tsx +108 -0
- package/dist/kits/blocks/components/ui/textarea.tsx +26 -0
- package/dist/kits/blocks/components/ui/theme-selector.tsx +247 -0
- package/dist/kits/blocks/components/ui/theme-toggle.tsx +74 -0
- package/dist/kits/blocks/components/ui/toaster.tsx +7 -0
- package/dist/kits/blocks/lib/themes.ts +399 -0
- package/dist/kits/blocks/lib/themes_old.ts +37 -0
- package/dist/kits/blocks/lib/utils.ts +9 -0
- package/dist/kits/blocks/next.config.ts +11 -0
- package/dist/kits/blocks/notes/THEME_GUIDE.md +29 -0
- package/dist/kits/blocks/notes/THEMING_CONVERSION_SUMMARY.md +14 -0
- package/dist/kits/blocks/package-deps.json +22 -0
- package/dist/kits/blocks/public/placeholders/gallery/hero-pexels-broken-9945014.avif +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626431.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626432.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626434.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626436.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_1.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_2.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_3.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_4.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/hero.png +0 -0
- package/dist/kits/blocks/public/placeholders/saas_dashboard/analytics.png +0 -0
- package/dist/kits/blocks/public/placeholders/saas_dashboard/chat.png +0 -0
- package/dist/kits/blocks/public/placeholders/saas_dashboard/projectBoard.png +0 -0
- package/dist/kits/data/.gitkeep +0 -0
- package/dist/kits/data/README.md +80 -0
- package/dist/kits/data/app/(protected)/admin/posts/page.tsx +5 -0
- package/dist/kits/data/app/(protected)/admin/users/page.tsx +5 -0
- package/dist/kits/data/app/api/posts/[id]/route.ts +83 -0
- package/dist/kits/data/app/api/posts/route.ts +138 -0
- package/dist/kits/data/app/api/seed-demo/route.ts +45 -0
- package/dist/kits/data/app/api/users/[id]/route.ts +127 -0
- package/dist/kits/data/app/api/users/check-email/route.ts +18 -0
- package/dist/kits/data/app/api/users/check-unique/route.ts +27 -0
- package/dist/kits/data/app/api/users/route.ts +79 -0
- package/dist/kits/data/app/examples/demo/README.md +4 -0
- package/dist/kits/data/app/examples/demo/create-post-form.tsx +106 -0
- package/dist/kits/data/app/examples/demo/page.tsx +118 -0
- package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +37 -0
- package/dist/kits/data/components/admin/posts-manager.tsx +719 -0
- package/dist/kits/data/components/admin/users-manager.tsx +432 -0
- package/dist/kits/data/lib/prisma.ts +15 -0
- package/dist/kits/data/lib/server/result.ts +90 -0
- package/dist/kits/data/package-deps.json +11 -0
- package/dist/kits/data/scripts/seed-demo.mjs +41 -0
- package/dist/kits/forms/.gitkeep +0 -0
- package/dist/kits/forms/README.md +49 -0
- package/dist/kits/forms/app/.gitkeep +0 -0
- package/dist/kits/forms/app/api/wizard/route.ts +71 -0
- package/dist/kits/forms/app/examples/forms/basic/page.tsx +124 -0
- package/dist/kits/forms/app/examples/forms/server-action/form-client.tsx +28 -0
- package/dist/kits/forms/app/examples/forms/server-action/page.tsx +71 -0
- package/dist/kits/forms/app/examples/forms/wizard/page.tsx +15 -0
- package/dist/kits/forms/app/examples/forms/wizard/wizard-client.tsx +2 -0
- package/dist/kits/forms/components/.gitkeep +0 -0
- package/dist/kits/forms/components/examples/wizard-client.tsx +231 -0
- package/dist/kits/forms/components/hooks/useCheckUnique.ts +79 -0
- package/dist/kits/forms/components/ui/button.tsx +122 -0
- package/dist/kits/forms/components/ui/checkbox.tsx +30 -0
- package/dist/kits/forms/components/ui/form/context.ts +33 -0
- package/dist/kits/forms/components/ui/form/form-control.tsx +28 -0
- package/dist/kits/forms/components/ui/form/form-description.tsx +22 -0
- package/dist/kits/forms/components/ui/form/form-field.tsx +36 -0
- package/dist/kits/forms/components/ui/form/form-item.tsx +21 -0
- package/dist/kits/forms/components/ui/form/form-label.tsx +24 -0
- package/dist/kits/forms/components/ui/form/form-message.tsx +29 -0
- package/dist/kits/forms/components/ui/form/form.tsx +26 -0
- package/dist/kits/forms/components/ui/input.tsx +27 -0
- package/dist/kits/forms/components/ui/label.tsx +29 -0
- package/dist/kits/forms/components/ui/select.tsx +25 -0
- package/dist/kits/forms/components/ui/switch.tsx +78 -0
- package/dist/kits/forms/components/ui/textarea.tsx +26 -0
- package/dist/kits/forms/lib/.gitkeep +0 -0
- package/dist/kits/forms/lib/forms/map-errors.ts +29 -0
- package/dist/kits/forms/lib/prisma.ts +16 -0
- package/dist/kits/forms/lib/utils.ts +9 -0
- package/dist/kits/forms/lib/validation/forms.ts +88 -0
- package/dist/kits/forms/lib/validation/wizard.ts +32 -0
- package/dist/kits/forms/package-deps.json +17 -0
- package/dist/utils/file-operations.d.ts +18 -0
- package/dist/utils/file-operations.d.ts.map +1 -0
- package/dist/utils/file-operations.js +327 -0
- package/dist/utils/file-operations.js.map +1 -0
- package/dist/utils/installation-tracker.d.ts +26 -0
- package/dist/utils/installation-tracker.d.ts.map +1 -0
- package/dist/utils/installation-tracker.js +98 -0
- package/dist/utils/installation-tracker.js.map +1 -0
- package/package.json +51 -21
- package/index.js +0 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { NextAuthOptions } from "next-auth";
|
|
2
|
+
import NextAuth from "next-auth";
|
|
3
|
+
import GitHubProvider from "next-auth/providers/github";
|
|
4
|
+
import CredentialsProvider from "next-auth/providers/credentials";
|
|
5
|
+
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
|
6
|
+
import { prisma } from "@/lib/prisma";
|
|
7
|
+
import { compare } from "bcryptjs";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { zodErrorToFieldErrors } from "@/lib/api/errors";
|
|
10
|
+
|
|
11
|
+
export const authOptions: NextAuthOptions = {
|
|
12
|
+
adapter: PrismaAdapter(prisma),
|
|
13
|
+
providers: [
|
|
14
|
+
CredentialsProvider({
|
|
15
|
+
name: "Credentials",
|
|
16
|
+
credentials: {
|
|
17
|
+
email: { label: "Email", type: "email" },
|
|
18
|
+
password: { label: "Password", type: "password" },
|
|
19
|
+
},
|
|
20
|
+
async authorize(credentials) {
|
|
21
|
+
const schema = z.object({
|
|
22
|
+
email: z.string().email("Invalid email address"),
|
|
23
|
+
password: z.string().min(1, "Password is required"),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const { email, password } = schema.parse(credentials ?? {});
|
|
28
|
+
|
|
29
|
+
const user = await prisma.user.findFirst({
|
|
30
|
+
where: {
|
|
31
|
+
email: {
|
|
32
|
+
equals: email,
|
|
33
|
+
mode: "insensitive",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
select: {
|
|
37
|
+
id: true,
|
|
38
|
+
name: true,
|
|
39
|
+
email: true,
|
|
40
|
+
password: true,
|
|
41
|
+
role: true,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!user || !user.password) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
message: "Invalid credentials",
|
|
49
|
+
errors: { email: "Email not found" },
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isValid = await compare(password, user.password);
|
|
55
|
+
if (!isValid) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
message: "Invalid credentials",
|
|
59
|
+
errors: { password: "Incorrect password" },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
id: user.id,
|
|
66
|
+
name: user.name,
|
|
67
|
+
email: user.email,
|
|
68
|
+
role: user.role,
|
|
69
|
+
} as any;
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
if (err instanceof z.ZodError) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
message: "Validation failed",
|
|
75
|
+
errors: zodErrorToFieldErrors(err),
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
JSON.parse(err?.message);
|
|
81
|
+
throw err; // already structured
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
message: "Login failed",
|
|
86
|
+
errors: { email: "Invalid email or password" },
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
...(process.env.GITHUB_ID && process.env.GITHUB_SECRET
|
|
94
|
+
? [
|
|
95
|
+
GitHubProvider({
|
|
96
|
+
clientId: process.env.GITHUB_ID!,
|
|
97
|
+
clientSecret: process.env.GITHUB_SECRET!,
|
|
98
|
+
}),
|
|
99
|
+
]
|
|
100
|
+
: []),
|
|
101
|
+
],
|
|
102
|
+
session: { strategy: "jwt" },
|
|
103
|
+
callbacks: {
|
|
104
|
+
async jwt({ token, user, trigger, session }) {
|
|
105
|
+
if (user) {
|
|
106
|
+
(token as any).id = (user as any).id;
|
|
107
|
+
token.name = user.name ?? "";
|
|
108
|
+
token.email = user.email ?? "";
|
|
109
|
+
(token as any).role =
|
|
110
|
+
(user as any).role ?? (token as any).role ?? "user";
|
|
111
|
+
}
|
|
112
|
+
if (trigger === "update" && session) {
|
|
113
|
+
const s = session as any;
|
|
114
|
+
if (typeof s.name === "string") token.name = s.name;
|
|
115
|
+
if (typeof s.email === "string") token.email = s.email;
|
|
116
|
+
if (typeof s.role === "string") (token as any).role = s.role;
|
|
117
|
+
if (s.image === null || typeof s.image === "string")
|
|
118
|
+
(token as any).image = s.image ?? null;
|
|
119
|
+
}
|
|
120
|
+
return token;
|
|
121
|
+
},
|
|
122
|
+
async session({ session, token }) {
|
|
123
|
+
if (session.user) {
|
|
124
|
+
(session.user as any).id = (token as any).id as string;
|
|
125
|
+
session.user.name = token.name ?? null;
|
|
126
|
+
session.user.email = token.email ?? "";
|
|
127
|
+
(session.user as any).role = (token as any).role ?? "user";
|
|
128
|
+
}
|
|
129
|
+
return session;
|
|
130
|
+
},
|
|
131
|
+
async redirect({ url, baseUrl }) {
|
|
132
|
+
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
|
133
|
+
if (new URL(url).origin === baseUrl) return url;
|
|
134
|
+
return baseUrl;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
pages: { signIn: "/auth/login" },
|
|
138
|
+
secret: process.env.NEXTAUTH_SECRET,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handler = NextAuth(authOptions);
|
|
142
|
+
export { handler as GET, handler as POST };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
import { sendSmtpEmail } from "@/lib/email/provider-smtp";
|
|
3
|
+
|
|
4
|
+
let cachedTransport: nodemailer.Transporter | null = null;
|
|
5
|
+
|
|
6
|
+
async function getDevTransport() {
|
|
7
|
+
if (cachedTransport) return cachedTransport;
|
|
8
|
+
const testAccount = await nodemailer.createTestAccount();
|
|
9
|
+
const transport = nodemailer.createTransport({
|
|
10
|
+
host: "smtp.ethereal.email",
|
|
11
|
+
port: 587,
|
|
12
|
+
auth: { user: testAccount.user, pass: testAccount.pass },
|
|
13
|
+
});
|
|
14
|
+
cachedTransport = transport;
|
|
15
|
+
return transport;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function sendDevEmail(opts: {
|
|
19
|
+
to: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
text?: string;
|
|
22
|
+
html?: string;
|
|
23
|
+
from?: string;
|
|
24
|
+
}) {
|
|
25
|
+
const smtpConfigured = !!(
|
|
26
|
+
process.env.SMTP_HOST && process.env.SMTP_USER && process.env.SMTP_PASS
|
|
27
|
+
);
|
|
28
|
+
if (smtpConfigured && process.env.NODE_ENV === "production") {
|
|
29
|
+
return sendSmtpEmail(opts).then((r) => ({ info: r.info, previewUrl: undefined }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const transport = await getDevTransport();
|
|
33
|
+
const info = await transport.sendMail({
|
|
34
|
+
from: opts.from ?? `Nextworks <${process.env.NOREPLY_EMAIL ?? "no-reply@example.com"}>`,
|
|
35
|
+
to: opts.to,
|
|
36
|
+
subject: opts.subject,
|
|
37
|
+
text: opts.text,
|
|
38
|
+
html: opts.html,
|
|
39
|
+
});
|
|
40
|
+
const previewUrl = nodemailer.getTestMessageUrl(info) || undefined;
|
|
41
|
+
return { info, previewUrl };
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sendDevEmail } from "@/lib/email/dev-transport";
|
|
2
|
+
import { sendSmtpEmail } from "@/lib/email/provider-smtp";
|
|
3
|
+
|
|
4
|
+
export async function sendEmail(opts: {
|
|
5
|
+
to: string;
|
|
6
|
+
subject: string;
|
|
7
|
+
text?: string;
|
|
8
|
+
html?: string;
|
|
9
|
+
from?: string;
|
|
10
|
+
}) {
|
|
11
|
+
const smtpConfigured = !!(
|
|
12
|
+
process.env.SMTP_HOST && process.env.SMTP_USER && process.env.SMTP_PASS
|
|
13
|
+
);
|
|
14
|
+
if (smtpConfigured) {
|
|
15
|
+
return sendSmtpEmail(opts);
|
|
16
|
+
}
|
|
17
|
+
if (process.env.NEXTWORKS_USE_DEV_EMAIL === "1") {
|
|
18
|
+
return sendDevEmail(opts);
|
|
19
|
+
}
|
|
20
|
+
throw new Error("No email transport configured");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isEmailProviderConfigured() {
|
|
24
|
+
return !!(
|
|
25
|
+
(process.env.SMTP_HOST && process.env.SMTP_USER && process.env.SMTP_PASS) ||
|
|
26
|
+
process.env.MAILER_API_KEY
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
|
|
3
|
+
export async function sendSmtpEmail(opts: {
|
|
4
|
+
to: string;
|
|
5
|
+
subject: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
html?: string;
|
|
8
|
+
from?: string;
|
|
9
|
+
}) {
|
|
10
|
+
const host = process.env.SMTP_HOST;
|
|
11
|
+
const port = parseInt(process.env.SMTP_PORT || "587", 10);
|
|
12
|
+
const secure = port === 465;
|
|
13
|
+
const user = process.env.SMTP_USER;
|
|
14
|
+
const pass = process.env.SMTP_PASS;
|
|
15
|
+
|
|
16
|
+
if (!host || !user || !pass) throw new Error("SMTP not configured");
|
|
17
|
+
|
|
18
|
+
const transporter = nodemailer.createTransport({
|
|
19
|
+
host,
|
|
20
|
+
port,
|
|
21
|
+
secure,
|
|
22
|
+
auth: { user, pass },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const info = await transporter.sendMail({
|
|
26
|
+
from:
|
|
27
|
+
opts.from ??
|
|
28
|
+
`No Reply <${process.env.NOREPLY_EMAIL ?? "no-reply@example.com"}>`,
|
|
29
|
+
to: opts.to,
|
|
30
|
+
subject: opts.subject,
|
|
31
|
+
text: opts.text,
|
|
32
|
+
html: opts.html,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return { info };
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function mapApiErrorsToForm(methods: any, payload: any) {
|
|
2
|
+
if (!payload || typeof payload !== "object") return null;
|
|
3
|
+
const errors = payload.errors || null;
|
|
4
|
+
const message = payload.message || null;
|
|
5
|
+
if (errors && typeof errors === "object") {
|
|
6
|
+
Object.entries(errors).forEach(([field, msg]) => {
|
|
7
|
+
methods.setError(field as any, { type: "server", message: String(msg) });
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return message;
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
|
+
|
|
5
|
+
const globalForPrisma = globalThis as unknown as {
|
|
6
|
+
prisma: PrismaClient | undefined;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const prisma =
|
|
10
|
+
globalForPrisma.prisma ??
|
|
11
|
+
new PrismaClient({
|
|
12
|
+
log: ["error", "warn"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function jsonOk(data?: any, opts?: { status?: number; message?: string }) {
|
|
2
|
+
return Response.json({ success: true, data: data ?? null, message: opts?.message ?? null }, { status: opts?.status ?? 200 });
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function jsonFail(
|
|
6
|
+
message: string,
|
|
7
|
+
opts?: { status?: number; code?: string | number; errors?: Record<string, string> | null },
|
|
8
|
+
) {
|
|
9
|
+
return Response.json(
|
|
10
|
+
{ success: false, data: null, message, code: opts?.code ?? null, errors: opts?.errors ?? null },
|
|
11
|
+
{ status: opts?.status ?? 400 },
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function jsonFromZod(err: any, opts?: { status?: number; message?: string }) {
|
|
16
|
+
const fieldErrors: Record<string, string> = {};
|
|
17
|
+
if (err?.issues && Array.isArray(err.issues)) {
|
|
18
|
+
for (const issue of err.issues) {
|
|
19
|
+
if (issue.path && issue.path.length > 0) {
|
|
20
|
+
fieldErrors[String(issue.path[0])] = issue.message;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return jsonFail(opts?.message || "Validation failed", {
|
|
25
|
+
status: opts?.status ?? 400,
|
|
26
|
+
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : null,
|
|
27
|
+
code: "VALIDATION_ERROR",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function jsonFromPrisma(err: any) {
|
|
32
|
+
// Basic unique violation mapping (P2002)
|
|
33
|
+
const code = (err && err.code) || "PRISMA_ERROR";
|
|
34
|
+
if (code === "P2002") {
|
|
35
|
+
const meta = err.meta || {};
|
|
36
|
+
const target = Array.isArray(meta.target) ? meta.target[0] : meta.target;
|
|
37
|
+
const field = typeof target === "string" ? target : "field";
|
|
38
|
+
return jsonFail("Unique constraint violation", {
|
|
39
|
+
status: 409,
|
|
40
|
+
errors: { [field]: `${field} already in use` },
|
|
41
|
+
code: "UNIQUE_CONSTRAINT",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return jsonFail("Database error", { status: 500, code });
|
|
45
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const signupSchema = z.object({
|
|
4
|
+
name: z.string().min(2, "Name must be at least 2 characters"),
|
|
5
|
+
email: z.string().email("Invalid email address"),
|
|
6
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const loginSchema = z.object({
|
|
10
|
+
email: z.string().email("Invalid email address"),
|
|
11
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Request body for forgot-password endpoint
|
|
15
|
+
export const forgotPasswordSchema = z.object({
|
|
16
|
+
email: z.string().email("Invalid email address"),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Reset password schema (token + new password + confirm)
|
|
20
|
+
export const resetPasswordSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
token: z.string().min(1, "Token is required"),
|
|
23
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
24
|
+
confirmPassword: z.string().min(6, "Confirm password is required"),
|
|
25
|
+
})
|
|
26
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
27
|
+
path: ["confirmPassword"],
|
|
28
|
+
message: "Passwords do not match",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const contactSchema = z.object({
|
|
32
|
+
name: z.string().min(2, "Name must be at least 2 characters"),
|
|
33
|
+
email: z.string().email("Invalid email address"),
|
|
34
|
+
subject: z.string().min(5, "Subject must be at least 5 characters"),
|
|
35
|
+
message: z.string().min(10, "Message must be at least 10 characters"),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const newsletterSchema = z.object({
|
|
39
|
+
name: z.string().optional(),
|
|
40
|
+
email: z.string().email("Invalid email address"),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const userSchema = z.object({
|
|
44
|
+
email: z.string().email("Invalid email address"),
|
|
45
|
+
name: z
|
|
46
|
+
.string()
|
|
47
|
+
.min(2, "Name must be at least 2 characters")
|
|
48
|
+
.optional()
|
|
49
|
+
.or(z.literal("")),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const userUpdateSchema = z.object({
|
|
53
|
+
email: z.string().email("Invalid email address").optional(),
|
|
54
|
+
name: z
|
|
55
|
+
.string()
|
|
56
|
+
.min(2, "Name must be at least 2 characters")
|
|
57
|
+
.optional()
|
|
58
|
+
.or(z.literal("")),
|
|
59
|
+
image: z.string().optional().or(z.literal("")),
|
|
60
|
+
password: z
|
|
61
|
+
.string()
|
|
62
|
+
.min(6, "Password must be at least 6 characters")
|
|
63
|
+
.optional()
|
|
64
|
+
.or(z.literal("")),
|
|
65
|
+
emailVerified: z
|
|
66
|
+
.union([z.date(), z.string().transform((s) => new Date(s)), z.null()])
|
|
67
|
+
.optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const postSchema = z.object({
|
|
71
|
+
title: z
|
|
72
|
+
.string()
|
|
73
|
+
.min(1, "Title is required")
|
|
74
|
+
.min(3, "Title must be at least 3 characters"),
|
|
75
|
+
content: z.string().optional().or(z.literal("")),
|
|
76
|
+
authorId: z.string().min(1, "Author ID is required"),
|
|
77
|
+
// published is optional in forms/API - defaults to false in the DB
|
|
78
|
+
published: z.boolean().optional(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type NewsletterFormValues = z.infer<typeof newsletterSchema>;
|
|
82
|
+
export type ContactFormValues = z.infer<typeof contactSchema>;
|
|
83
|
+
export type SignupFormValues = z.infer<typeof signupSchema>;
|
|
84
|
+
export type LoginFormValues = z.infer<typeof loginSchema>;
|
|
85
|
+
export type UserFormValues = z.infer<typeof userSchema>;
|
|
86
|
+
export type PostFormValues = z.infer<typeof postSchema>;
|
|
87
|
+
export type ForgotPasswordFormValues = z.infer<typeof forgotPasswordSchema>;
|
|
88
|
+
export type ResetPasswordFormValues = z.infer<typeof resetPasswordSchema>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@next-auth/prisma-adapter": "^1.0.7",
|
|
4
|
+
"@prisma/client": "^6.16.1",
|
|
5
|
+
"@radix-ui/react-label": "^2.1.7",
|
|
6
|
+
"@radix-ui/react-slot": "^1.2.3",
|
|
7
|
+
"bcryptjs": "^3.0.2",
|
|
8
|
+
"class-variance-authority": "^0.7.1",
|
|
9
|
+
"clsx": "^2.1.1",
|
|
10
|
+
"lucide-react": "^0.542.0",
|
|
11
|
+
"next-auth": "^4.24.13",
|
|
12
|
+
"tailwind-merge": "^3.3.1",
|
|
13
|
+
"nodemailer": "^7.0.7",
|
|
14
|
+
"@nextworks/blocks-core": "*"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"prisma": "^6.16.1"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ========================================
|
|
2
|
+
// AUTH MODELS FOR NEXT-AUTH + PRISMA
|
|
3
|
+
// ========================================
|
|
4
|
+
//
|
|
5
|
+
// Copy these models to your prisma/schema.prisma file
|
|
6
|
+
//
|
|
7
|
+
// Required models for NextAuth.js with Prisma adapter:
|
|
8
|
+
// - User: Main user model
|
|
9
|
+
// - Account: OAuth provider accounts (GitHub, Google, etc.)
|
|
10
|
+
// - Session: User sessions
|
|
11
|
+
// - VerificationToken: Email verification tokens
|
|
12
|
+
//
|
|
13
|
+
// Optional models:
|
|
14
|
+
// - Post: Example model showing user relationships
|
|
15
|
+
//
|
|
16
|
+
// ========================================
|
|
17
|
+
|
|
18
|
+
model User {
|
|
19
|
+
id String @id @default(cuid())
|
|
20
|
+
email String @unique
|
|
21
|
+
name String?
|
|
22
|
+
image String?
|
|
23
|
+
emailVerified DateTime?
|
|
24
|
+
password String? // For credentials provider
|
|
25
|
+
createdAt DateTime @default(now())
|
|
26
|
+
|
|
27
|
+
// Relations
|
|
28
|
+
posts Post[]
|
|
29
|
+
accounts Account[]
|
|
30
|
+
sessions Session[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
model Account {
|
|
34
|
+
id String @id @default(cuid())
|
|
35
|
+
userId String
|
|
36
|
+
type String
|
|
37
|
+
provider String
|
|
38
|
+
providerAccountId String
|
|
39
|
+
access_token String?
|
|
40
|
+
token_type String?
|
|
41
|
+
scope String?
|
|
42
|
+
|
|
43
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
44
|
+
|
|
45
|
+
@@unique([provider, providerAccountId])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
model Session {
|
|
49
|
+
id String @id @default(cuid())
|
|
50
|
+
sessionToken String @unique
|
|
51
|
+
userId String
|
|
52
|
+
expires DateTime
|
|
53
|
+
|
|
54
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
model VerificationToken {
|
|
58
|
+
identifier String
|
|
59
|
+
token String @unique
|
|
60
|
+
expires DateTime
|
|
61
|
+
|
|
62
|
+
@@unique([identifier, token])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ========================================
|
|
66
|
+
// EXAMPLE MODEL (OPTIONAL)
|
|
67
|
+
// ========================================
|
|
68
|
+
// This is an example model showing how to create
|
|
69
|
+
// relationships with the User model. You can remove
|
|
70
|
+
// this if you don't need posts in your app.
|
|
71
|
+
|
|
72
|
+
model Post {
|
|
73
|
+
id String @id @default(cuid())
|
|
74
|
+
title String
|
|
75
|
+
content String?
|
|
76
|
+
published Boolean @default(false)
|
|
77
|
+
createdAt DateTime @default(now())
|
|
78
|
+
updatedAt DateTime @updatedAt
|
|
79
|
+
authorId String
|
|
80
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
81
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model User {
|
|
11
|
+
id String @id @default(cuid())
|
|
12
|
+
email String @unique
|
|
13
|
+
name String?
|
|
14
|
+
image String?
|
|
15
|
+
emailVerified DateTime?
|
|
16
|
+
password String?
|
|
17
|
+
role String @default("user")
|
|
18
|
+
createdAt DateTime @default(now())
|
|
19
|
+
posts Post[]
|
|
20
|
+
accounts Account[]
|
|
21
|
+
sessions Session[]
|
|
22
|
+
// Relation for password reset tokens
|
|
23
|
+
passwordResets PasswordReset[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
model Account {
|
|
27
|
+
id String @id @default(cuid())
|
|
28
|
+
userId String
|
|
29
|
+
type String
|
|
30
|
+
provider String
|
|
31
|
+
providerAccountId String
|
|
32
|
+
access_token String?
|
|
33
|
+
token_type String?
|
|
34
|
+
scope String?
|
|
35
|
+
|
|
36
|
+
user User @relation(fields: [userId], references: [id])
|
|
37
|
+
|
|
38
|
+
@@unique([provider, providerAccountId])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
model Session {
|
|
42
|
+
id String @id @default(cuid())
|
|
43
|
+
sessionToken String @unique
|
|
44
|
+
userId String
|
|
45
|
+
expires DateTime
|
|
46
|
+
|
|
47
|
+
user User @relation(fields: [userId], references: [id])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
model VerificationToken {
|
|
51
|
+
identifier String
|
|
52
|
+
token String @unique
|
|
53
|
+
expires DateTime
|
|
54
|
+
|
|
55
|
+
@@unique([identifier, token])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
model Post {
|
|
59
|
+
id String @id @default(cuid())
|
|
60
|
+
title String
|
|
61
|
+
slug String? @unique
|
|
62
|
+
content String?
|
|
63
|
+
excerpt String?
|
|
64
|
+
tags String?
|
|
65
|
+
published Boolean @default(false)
|
|
66
|
+
createdAt DateTime @default(now())
|
|
67
|
+
updatedAt DateTime @updatedAt
|
|
68
|
+
authorId String
|
|
69
|
+
author User @relation(fields: [authorId], references: [id])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
model PasswordReset {
|
|
73
|
+
id String @id @default(cuid())
|
|
74
|
+
token String?
|
|
75
|
+
tokenHash String? @unique
|
|
76
|
+
userId String
|
|
77
|
+
expires DateTime
|
|
78
|
+
used Boolean @default(false)
|
|
79
|
+
|
|
80
|
+
user User @relation(fields: [userId], references: [id])
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
|
|
6
|
+
const prisma = new PrismaClient();
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
console.log("Reading existing PasswordReset rows with token column...");
|
|
10
|
+
const rows = await prisma.$queryRaw`SELECT id, token FROM "PasswordReset" WHERE token IS NOT NULL`;
|
|
11
|
+
console.log(`Found ${rows.length} rows`);
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
const id = row.id;
|
|
14
|
+
const token = row.token;
|
|
15
|
+
if (!token) continue;
|
|
16
|
+
const hash = createHash("sha256").update(token).digest("hex");
|
|
17
|
+
await prisma.passwordReset.update({ where: { id }, data: { tokenHash: hash } });
|
|
18
|
+
console.log(`Updated ${id}`);
|
|
19
|
+
}
|
|
20
|
+
console.log("Done.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch((e) => {
|
|
24
|
+
console.error(e);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}).finally(async () => await prisma.$disconnect());
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
|
+
|
|
5
|
+
const prisma = new PrismaClient();
|
|
6
|
+
|
|
7
|
+
async function promote(email) {
|
|
8
|
+
if (!email) {
|
|
9
|
+
console.error("Usage: node scripts/promote-admin.mjs <email>");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
14
|
+
if (!user) {
|
|
15
|
+
console.error(`User not found: ${email}`);
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (user.role === "admin") {
|
|
20
|
+
console.log(`User ${email} is already an admin`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await prisma.user.update({ where: { email }, data: { role: "admin" } });
|
|
25
|
+
console.log(`Promoted ${email} to admin`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const email = process.argv[2];
|
|
30
|
+
promote(email).catch((err) => {
|
|
31
|
+
console.error(err);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|