diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5ef6a52
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..a461c50
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1 @@
+@import "tailwindcss";
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..93b31a5
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,22 @@
+import Footer from "@/components/Footer";
+import GithubLink from "@/components/GithubLink";
+import { metadata } from "@/metadata.config";
+import "./globals.css";
+
+export { metadata };
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..23bc83e
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import Actions from "@/components/Actions";
+import Background from "@/components/Background";
+import Header from "@/components/Header";
+import SecretCard from "@/components/SecretCard";
+import SecurityNotice from "@/components/SecurityNotice";
+import { copyToClipboard } from "@/lib/copyToClipboard";
+import { generateSecret } from "@/lib/generateSecret";
+import { motion } from "framer-motion";
+import { useEffect, useState } from "react";
+import { Toaster, toast } from "react-hot-toast";
+
+export default function Page() {
+ const [secret, setSecret] = useState("");
+ const [copied, setCopied] = useState(false);
+ const [copiedEnv, setCopiedEnv] = useState(false);
+
+ useEffect(() => setSecret(generateSecret()), []);
+
+ const handleGenerate = () => {
+ const s = generateSecret();
+ setSecret(s);
+ setCopied(false);
+ setCopiedEnv(false);
+ toast.success("New secret generated!", { id: "generate", duration: 1200 });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
copyToClipboard(`NEXTAUTH_SECRET=${secret}`, setCopiedEnv, ".env")}
+ />
+
+ copyToClipboard(secret, setCopied, "Secret")}
+ />
+
+
+ Copies: secret only or the full{" "}
+ NEXTAUTH_SECRET=... line.
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/Actions.tsx b/components/Actions.tsx
new file mode 100644
index 0000000..f850622
--- /dev/null
+++ b/components/Actions.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { motion } from "framer-motion";
+import { Check, Copy, RefreshCw } from "lucide-react";
+
+type Props = {
+ secret: string;
+ copiedEnv: boolean;
+ onGenerate: () => void;
+ onCopyEnv: () => void;
+};
+
+export default function Actions({ secret, copiedEnv, onGenerate, onCopyEnv }: Props) {
+ return (
+
+
+
+ Generate
+
+
+
+ .env
+ {copiedEnv ? : }
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/Background.tsx b/components/Background.tsx
new file mode 100644
index 0000000..088823c
--- /dev/null
+++ b/components/Background.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+export default function Background() {
+ return (
+
+ );
+}
diff --git a/components/Footer.tsx b/components/Footer.tsx
new file mode 100644
index 0000000..8e543a1
--- /dev/null
+++ b/components/Footer.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { FaHeart } from "react-icons/fa";
+
+export default function Footer() {
+ return (
+
+ );
+}
diff --git a/components/GithubLink.tsx b/components/GithubLink.tsx
new file mode 100644
index 0000000..f2da2c8
--- /dev/null
+++ b/components/GithubLink.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { motion } from "framer-motion";
+import Link from "next/link";
+import { useState } from "react";
+import { FaGithub } from "react-icons/fa";
+
+export default function GithubLink() {
+ const [hover, setHover] = useState(false);
+
+ return (
+
+ setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ className="group relative flex items-center justify-center"
+ >
+ {/* Icône GitHub */}
+
+
+
+
+ {/* Tooltip au survol */}
+
+ View on GitHub
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/Header.tsx b/components/Header.tsx
new file mode 100644
index 0000000..5d55c3e
--- /dev/null
+++ b/components/Header.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import { motion } from "framer-motion";
+import { Shield } from "lucide-react";
+
+export default function Header() {
+ return (
+
+
+
+ secure • 32 bytes Base64
+
+
+ Auth secret generator
+
+
+ Generate a cryptographically-secure secret for your Next.js apps.
+
+
+ );
+}
diff --git a/components/SecretCard.tsx b/components/SecretCard.tsx
new file mode 100644
index 0000000..8409d17
--- /dev/null
+++ b/components/SecretCard.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { AnimatePresence, motion } from "framer-motion";
+import { Check, Copy } from "lucide-react";
+
+type Props = {
+ secret: string;
+ copied: boolean;
+ onCopy: () => void;
+};
+
+export default function SecretCard({ secret, copied, onCopy }: Props) {
+ return (
+
+
+
+
+ {secret}
+
+
+
+
+ {copied ? : }
+
+
+
+ );
+}
diff --git a/components/SecurityNotice.tsx b/components/SecurityNotice.tsx
new file mode 100644
index 0000000..1d27aa5
--- /dev/null
+++ b/components/SecurityNotice.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { motion } from "framer-motion";
+import { Lock } from "lucide-react";
+
+export default function SecurityNotice() {
+ return (
+
+
+
+ No secrets generated are saved or transmitted.
+ Everything is generated locally in your browser.
+
+
+ );
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..719cea2
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,25 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
+ {
+ ignores: [
+ "node_modules/**",
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
+ ],
+ },
+];
+
+export default eslintConfig;
diff --git a/eslint.config.mts b/eslint.config.mts
new file mode 100644
index 0000000..673b075
--- /dev/null
+++ b/eslint.config.mts
@@ -0,0 +1,11 @@
+import js from "@eslint/js";
+import globals from "globals";
+import tseslint from "typescript-eslint";
+import pluginReact from "eslint-plugin-react";
+import { defineConfig } from "eslint/config";
+
+export default defineConfig([
+ { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
+ tseslint.configs.recommended,
+ pluginReact.configs.flat.recommended,
+]);
diff --git a/lib/copyToClipboard.ts b/lib/copyToClipboard.ts
new file mode 100644
index 0000000..ba86515
--- /dev/null
+++ b/lib/copyToClipboard.ts
@@ -0,0 +1,19 @@
+import { toast } from "react-hot-toast";
+
+export async function copyToClipboard(
+ value: string,
+ setFlag: (b: boolean) => void,
+ label?: string
+) {
+ try {
+ await navigator.clipboard.writeText(value);
+ setFlag(true);
+ toast.success(label ? `${label} copied!` : "Copied!", {
+ id: label ? `copy-${label}` : "copy",
+ duration: 1200,
+ });
+ setTimeout(() => setFlag(false), 1200);
+ } catch {
+ toast.error("Error during copying");
+ }
+}
diff --git a/lib/generateSecret.ts b/lib/generateSecret.ts
new file mode 100644
index 0000000..c46082b
--- /dev/null
+++ b/lib/generateSecret.ts
@@ -0,0 +1,7 @@
+export function generateSecret(bytes = 32): string {
+ const arr = new Uint8Array(bytes);
+ crypto.getRandomValues(arr);
+ let bin = "";
+ arr.forEach((b) => (bin += String.fromCharCode(b)));
+ return btoa(bin);
+}
\ No newline at end of file
diff --git a/metadata.config.ts b/metadata.config.ts
new file mode 100644
index 0000000..baf1c4b
--- /dev/null
+++ b/metadata.config.ts
@@ -0,0 +1,37 @@
+import type { Metadata, Viewport } from "next";
+
+export const metadata: Metadata = {
+ title: "Auth Secret Generator",
+ description:
+ "Generate cryptographically secure secrets for your Next.js apps. No data is stored or transmitted.",
+ icons: {
+ icon: "/favicon.png",
+ },
+ openGraph: {
+ title: "Auth Secret Generator",
+ description:
+ "Generate cryptographically secure secrets for your Next.js apps. No data is stored or transmitted.",
+ url: "https://nasg.ultralion.xyz",
+ siteName: "Auth Secret Generator",
+ type: "website",
+ },
+ twitter: {
+ card: "summary",
+ title: "Auth Secret Generator",
+ description:
+ "Generate cryptographically secure secrets for your Next.js apps. No data is stored or transmitted.",
+ },
+ robots: {
+ index: true,
+ follow: true,
+ nocache: false,
+ googleBot: {
+ index: true,
+ follow: true,
+ },
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: "#0f172a",
+};
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
new file mode 100644
index 0000000..e9ffa30
--- /dev/null
+++ b/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+};
+
+export default nextConfig;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..956a5af
--- /dev/null
+++ b/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "nasg",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Auth Secret Generator — Generate cryptographically secure secrets for your Next.js apps. No data is stored or transmitted.",
+ "author": {
+ "name": "UltraLion",
+ "email": "ultralionfr@gmail.com",
+ "url": "https://ultralion.xyz"
+ },
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/UltraLionfr/nasg.git"
+ },
+ "bugs": {
+ "url": "https://github.com/UltraLionfr/nasg/issues"
+ },
+ "homepage": "https://nasg.ultralion.xyz",
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.12",
+ "framer-motion": "^12.23.12",
+ "lucide-react": "^0.540.0",
+ "next": "15.5.0",
+ "postcss": "^8.5.6",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "react-hot-toast": "^2.6.0",
+ "react-icons": "^5.5.0",
+ "tailwindcss": "^4.1.12"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3",
+ "@eslint/js": "^9.33.0",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9.33.0",
+ "eslint-config-next": "15.5.0",
+ "eslint-plugin-react": "^7.37.5",
+ "globals": "^16.3.0",
+ "tw-animate-css": "^1.3.7",
+ "typescript": "^5",
+ "typescript-eslint": "^8.40.0"
+ },
+ "engines": {
+ "node": ">=18.18.0",
+ "pnpm": ">=9.0.0"
+ },
+ "packageManager": "pnpm@9.12.3"
+}
diff --git a/postcss.config.mjs b/postcss.config.mjs
new file mode 100644
index 0000000..54e8739
--- /dev/null
+++ b/postcss.config.mjs
@@ -0,0 +1,6 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+export default config;
\ No newline at end of file
diff --git a/public/favicon.png b/public/favicon.png
new file mode 100644
index 0000000..f887ac8
Binary files /dev/null and b/public/favicon.png differ
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..d8b9323
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}