feat: initial release of NASG

This commit is contained in:
UltraLionFr
2025-08-20 22:56:16 +02:00
parent 34af26c8ea
commit 586c1a9720
21 changed files with 540 additions and 0 deletions
+41
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+22
View File
@@ -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 (
<html lang="en" className="scroll-smooth">
<body className="min-h-screen bg-neutral-950 text-white antialiased">
<GithubLink />
{children}
<Footer />
</body>
</html>
);
}
+69
View File
@@ -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 (
<main className="relative min-h-screen flex items-center justify-center p-6 text-white">
<Background />
<motion.div
className="w-full max-w-3xl"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<div className="relative mx-auto rounded-3xl border border-white/10 bg-white/5 p-8 shadow-[0_10px_30px_-10px_rgba(0,0,0,.5)] backdrop-blur-xl">
<div className="pointer-events-none absolute inset-x-8 -top-0.5 h-px bg-gradient-to-r from-transparent via-sky-400/60 to-transparent" />
<Header />
<Actions
secret={secret}
copiedEnv={copiedEnv}
onGenerate={handleGenerate}
onCopyEnv={() => copyToClipboard(`NEXTAUTH_SECRET=${secret}`, setCopiedEnv, ".env")}
/>
<SecretCard
secret={secret}
copied={copied}
onCopy={() => copyToClipboard(secret, setCopied, "Secret")}
/>
<p className="mt-3 text-center text-xs text-neutral-400">
Copies: secret only or the full{" "}
<code className="rounded bg-white/10 px-1 py-0.5">NEXTAUTH_SECRET=...</code> line.
</p>
<SecurityNotice />
</div>
</motion.div>
<Toaster position="bottom-center" />
</main>
);
}
+46
View File
@@ -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 (
<motion.div
className="mt-8 flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-center"
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, duration: 0.5, ease: "easeOut" }}
>
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
onClick={onGenerate}
className="group inline-flex justify-center items-center gap-2 rounded-2xl border border-sky-500/30 bg-sky-500/10 px-5 py-2.5 text-sm font-semibold text-sky-300 ring-sky-500/40 transition hover:bg-sky-500/15 focus:outline-none focus:ring-4"
>
<RefreshCw className="h-4 w-4 transition group-hover:rotate-90" />
Generate
</motion.button>
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
onClick={onCopyEnv}
className="inline-flex justify-center items-center gap-2 rounded-2xl border border-white/10 bg-white/10 px-5 py-2.5 text-sm font-medium shadow-sm transition hover:bg-white/15"
aria-label="Copy .env line"
title={copiedEnv ? "Copied" : "Copy .env"}
>
<span className="font-semibold">.env</span>
{copiedEnv ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</motion.button>
</motion.div>
);
}
+12
View File
@@ -0,0 +1,12 @@
"use client";
export default function Background() {
return (
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-br from-[#0f172a] via-[#1e293b] to-[#0f172a]" />
<div
className="absolute inset-0 bg-[url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2240%22 height=%2240%22 fill=%22none%22 stroke=%22%233459a1%22 stroke-width=%220.4%22><path d=%22M0 .5H40M.5 0V40%22/></svg>')] opacity-[0.05]"
/>
</div>
);
}
+21
View File
@@ -0,0 +1,21 @@
"use client";
import { FaHeart } from "react-icons/fa";
export default function Footer() {
return (
<footer className="fixed bottom-4 left-1/2 -translate-x-1/2 text-xs text-neutral-400 flex items-center gap-1">
<span>Made with</span>
<FaHeart className="text-red-500 animate-pulse" />
<span>by</span>
<a
href="https://ultralion.xyz"
target="_blank"
rel="noopener noreferrer"
className="font-medium text-white hover:text-sky-400 transition"
>
UltraLion
</a>
</footer>
);
}
+42
View File
@@ -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 (
<div className="fixed top-4 right-4 z-50">
<Link
href="https://github.com/UltraLionfr/nasg"
target="_blank"
rel="noopener noreferrer"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
className="group relative flex items-center justify-center"
>
{/* Icône GitHub */}
<motion.div
whileHover={{ scale: 1.1, rotate: 5 }}
whileTap={{ scale: 0.95 }}
className="rounded-full border border-white/10 bg-black/60 p-2 backdrop-blur transition-colors group-hover:bg-black/80"
>
<FaGithub className="h-5 w-5 text-white" />
</motion.div>
{/* Tooltip au survol */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={hover ? { opacity: 1, y: 0 } : { opacity: 0, y: 10 }}
transition={{ duration: 0.2 }}
className="absolute right-10 top-1/2 -translate-y-1/2 whitespace-nowrap rounded-lg border border-white/10 bg-black/80 px-3 py-1.5 text-xs text-white shadow-lg backdrop-blur"
>
View on GitHub
</motion.div>
</Link>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
"use client";
import { motion } from "framer-motion";
import { Shield } from "lucide-react";
export default function Header() {
return (
<motion.div
className="text-center"
initial={{ opacity: 0, y: -15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.5, ease: "easeOut" }}
>
<div className="mx-auto mb-3 inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs font-medium text-neutral-200 backdrop-blur-sm">
<Shield className="h-3.5 w-3.5" />
secure 32 bytes Base64
</div>
<h1 className="text-3xl md:text-5xl font-extrabold tracking-tight text-white">
Auth secret generator
</h1>
<p className="mt-2 text-sm text-neutral-300">
Generate a cryptographically-secure secret for your Next.js apps.
</p>
</motion.div>
);
}
+43
View File
@@ -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 (
<div className="mt-6 rounded-2xl border border-white/10 bg-black/40 text-white shadow-md">
<div className="flex items-center gap-3 p-4">
<AnimatePresence mode="wait">
<motion.div
key={secret}
className="flex-1 max-h-24 overflow-y-auto break-all font-mono text-sm opacity-90"
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{secret}
</motion.div>
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
onClick={onCopy}
className="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/5 p-2 transition hover:bg-white/10"
aria-label="Copy secret"
title={copied ? "Copied" : "Copy"}
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</motion.button>
</div>
</div>
);
}
+21
View File
@@ -0,0 +1,21 @@
"use client";
import { motion } from "framer-motion";
import { Lock } from "lucide-react";
export default function SecurityNotice() {
return (
<motion.div
className="mt-4 flex items-start gap-2 rounded-xl border border-sky-500/30 bg-black/50 p-3 font-mono text-xs text-sky-300"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.5, ease: "easeOut" }}
>
<Lock className="h-4 w-4 mt-0.5 shrink-0 text-sky-400" />
<p>
No secrets generated are saved or transmitted.
Everything is generated <span className="text-white font-semibold">locally</span> in your browser.
</p>
</motion.div>
);
}
+25
View File
@@ -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;
+11
View File
@@ -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,
]);
+19
View File
@@ -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");
}
}
+7
View File
@@ -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);
}
+37
View File
@@ -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",
};
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+57
View File
@@ -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"
}
+6
View File
@@ -0,0 +1,6 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+27
View File
@@ -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"]
}