├── app ├── robots.txt ├── favicon.ico ├── api │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ ├── change-password │ │ │ └── route.ts │ │ └── register │ │ │ └── route.ts │ ├── devices │ │ ├── [deviceId] │ │ │ ├── qr │ │ │ │ └── route.ts │ │ │ ├── config │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── route.ts │ └── admin │ │ ├── invite-codes │ │ ├── [codeId] │ │ │ ├── usage │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── route.ts │ │ └── users │ │ └── route.ts ├── (unauthenticated) │ ├── (marketing) │ │ ├── (pages) │ │ │ ├── about │ │ │ │ └── page.tsx │ │ │ ├── contact │ │ │ │ └── page.tsx │ │ │ ├── pricing │ │ │ │ └── page.tsx │ │ │ ├── features │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── _components │ │ │ ├── header-wrapper.tsx │ │ │ ├── sections │ │ │ │ ├── section-wrapper.tsx │ │ │ │ ├── cta-section.tsx │ │ │ │ ├── social-proof-section.tsx │ │ │ │ ├── companies-section.tsx │ │ │ │ ├── faq-section.tsx │ │ │ │ ├── features-section.tsx │ │ │ │ └── video-section.tsx │ │ │ ├── site-banner.tsx │ │ │ ├── scroll-indicator.tsx │ │ │ ├── sticky-cta.tsx │ │ │ └── footer.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ └── confirmation │ │ └── page.tsx ├── (authenticated) │ └── dashboard │ │ ├── layout.tsx │ │ ├── (pages) │ │ └── devices │ │ │ └── new │ │ │ └── page.tsx │ │ └── _components │ │ ├── app-sidebar.tsx │ │ ├── team-switcher.tsx │ │ └── nav-user.tsx ├── not-found.tsx ├── layout.tsx └── page.tsx ├── postcss.config.mjs ├── supabase └── .gitignore ├── db ├── migrations │ ├── 0002_add_last_login.sql │ ├── 0003_add_invite_code_active_status.sql │ ├── meta │ │ └── _journal.json │ ├── 0000_giant_butterfly.sql │ └── 0001_multi_use_invites.sql ├── index.ts ├── seed │ ├── index.ts │ └── data │ │ └── customers.ts ├── schema.ts └── migrate-to-multi-use.js ├── lib ├── utils.ts ├── language-context.tsx └── wireguard.ts ├── auth.ts ├── drizzle.config.ts ├── components ├── ui │ ├── skeleton.tsx │ ├── sonner.tsx │ ├── label.tsx │ ├── separator.tsx │ ├── textarea.tsx │ ├── collapsible.tsx │ ├── input.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── card.tsx │ ├── tooltip.tsx │ ├── button.tsx │ ├── breadcrumb.tsx │ ├── table.tsx │ ├── dialog.tsx │ ├── alert-dialog.tsx │ └── sheet.tsx └── utility │ ├── tailwind-indicator.tsx │ └── markdown.tsx ├── .github ├── config.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── feature_request.yml │ ├── bug_report.md │ └── bug_report.yml └── PULL_REQUEST_TEMPLATE.md ├── env.example ├── components.json ├── types └── next-auth.d.ts ├── Dockerfile ├── .repo_ignore ├── SECURITY.md ├── eslint.config.mjs ├── hooks ├── use-mobile.ts └── use-local-storage.ts ├── tsconfig.json ├── .gitignore ├── .env.example ├── middleware.ts ├── docker-compose.yml ├── prettier.config.cjs ├── CHANGELOG.md ├── license ├── next.config.ts ├── ADMIN_GUIDE.md ├── TROUBLESHOOTING.md ├── ROADMAP.md ├── CONTRIBUTING.md ├── auth.config.ts ├── package.json ├── CLAUDE.md └── CODE_OF_CONDUCT.md /app/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashvakil/LeiaGuard/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "../../../../auth"; 2 | 3 | export const { GET, POST } = handlers; -------------------------------------------------------------------------------- /db/migrations/0002_add_last_login.sql: -------------------------------------------------------------------------------- 1 | -- Add last login tracking to users table 2 | ALTER TABLE users ADD COLUMN last_login_at TEXT; -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/(pages)/about/page.tsx: -------------------------------------------------------------------------------- 1 | export default function AboutPage() { 2 | return ( 3 |
4 |

About

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/_components/header-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "./header" 2 | 3 | export async function HeaderWrapper() { 4 | return
5 | } 6 | -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/(pages)/contact/page.tsx: -------------------------------------------------------------------------------- 1 | export default function ContactPage() { 2 | return ( 3 |
4 |

Contact

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/(pages)/pricing/page.tsx: -------------------------------------------------------------------------------- 1 | export default function PricingPage() { 2 | return ( 3 |
4 |

Pricing

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /db/migrations/0003_add_invite_code_active_status.sql: -------------------------------------------------------------------------------- 1 | -- Add is_active column to invitation_codes table 2 | ALTER TABLE `invitation_codes` ADD COLUMN `is_active` integer DEFAULT 1 NOT NULL; -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/(pages)/features/page.tsx: -------------------------------------------------------------------------------- 1 | export default function FeaturesPage() { 2 | return ( 3 |
4 |

Features

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import authConfig from "./auth.config"; 3 | 4 | export const { 5 | handlers, 6 | auth, 7 | signIn, 8 | signOut, 9 | } = NextAuth({ 10 | ...authConfig, 11 | secret: process.env.NEXTAUTH_SECRET, 12 | }); 13 | 14 | export const { GET, POST } = handlers; -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/(pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function MarketingPagesLayout({ 2 | children 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | config({ path: ".env.local" }); 5 | 6 | export default defineConfig({ 7 | out: "./db/migrations", 8 | schema: "./db/schema.ts", 9 | dialect: "sqlite", 10 | dbCredentials: { 11 | url: "./db/wireguard.db", 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/arashvakil/LeiaGuard/discussions/new?category=q-a 5 | about: Ask a question about the project or how to use it. 6 | - name: Request a feature 7 | url: https://github.com/arashvakil/LeiaGuard/discussions/new?category=ideas 8 | about: Suggest an idea for this project. 9 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET=CHANGE_ME 2 | NEXTAUTH_URL=http://localhost:3000 3 | AUTH_TRUST_HOST=true 4 | 5 | # WireGuard server configuration 6 | WIREGUARD_SERVER_IP=YOUR_SERVER_IP 7 | WIREGUARD_SERVER_DOMAIN=example.com 8 | WIREGUARD_SERVER_PUBLIC_KEY=YOUR_PUBLIC_KEY 9 | WIREGUARD_SERVER_PORT=51820 10 | WIREGUARD_NETWORK_RANGE=10.0.0.0/24 11 | 12 | # Database (SQLite by default) 13 | DATABASE_URL=file:./db/wireguard.db -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/_components/sections/section-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | interface SectionWrapperProps { 4 | children: React.ReactNode 5 | className?: string 6 | id?: string 7 | } 8 | 9 | export function SectionWrapper({ 10 | children, 11 | className, 12 | id 13 | }: SectionWrapperProps) { 14 | return ( 15 |
16 | {children} 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { DefaultSession } from "next-auth"; 2 | 3 | declare module "next-auth" { 4 | interface Session { 5 | user: { 6 | id: string; 7 | username: string; 8 | isAdmin: boolean; 9 | } & DefaultSession["user"]; 10 | } 11 | 12 | interface User { 13 | id: string; 14 | username: string; 15 | isAdmin: boolean; 16 | } 17 | } 18 | 19 | declare module "next-auth/jwt" { 20 | interface JWT { 21 | isAdmin: boolean; 22 | username: string; 23 | } 24 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js 20 image as the base image 2 | FROM node:20-alpine 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json to the working directory 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code to the working directory 14 | COPY . . 15 | 16 | # Build the Next.js application 17 | RUN npm run build 18 | 19 | # Expose the port the app runs on 20 | EXPOSE 3000 21 | 22 | # Command to run the application 23 | CMD ["npm", "start"] 24 | -------------------------------------------------------------------------------- /.repo_ignore: -------------------------------------------------------------------------------- 1 | # Package manager caches 2 | **/node_modules/ 3 | **/.npm/ 4 | **/__pycache__/ 5 | **/.pytest_cache/ 6 | **/.mypy_cache/ 7 | 8 | # Build caches 9 | **/.gradle/ 10 | **/.nuget/ 11 | **/.cargo/ 12 | **/.stack-work/ 13 | **/.ccache/ 14 | 15 | # IDE and Editor caches 16 | **/.idea/ 17 | **/.vscode/ 18 | **/*.swp 19 | **/*~ 20 | 21 | # Temp files 22 | **/*.tmp 23 | **/*.temp 24 | **/*.bak 25 | 26 | **/*.meta 27 | **/package-lock.json 28 | 29 | # AI Specific 30 | .repo_ignore 31 | .cursorrules 32 | 33 | # Project Specific 34 | **/.github 35 | **/.husky 36 | **/migrations 37 | **/public 38 | **/.next 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities as soon as possible. Only the latest minor version is officially supported. 6 | 7 | | Version | Supported | 8 | | ------- | --------- | 9 | | latest | ✅ | 10 | | < latest | ❌ | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please email **security@yourdomain.com** with details. We will respond within **72 hours** (usually much sooner). 15 | 16 | Disclosing security issues publicly without giving maintainers a chance to respond is discouraged – please follow responsible disclosure practices. -------------------------------------------------------------------------------- /db/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1750767805760, 9 | "tag": "0000_giant_butterfly", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "6", 15 | "when": 1750770263500, 16 | "tag": "0001_multi_use_invites", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "6", 22 | "when": 1750781000000, 23 | "tag": "0003_add_invite_code_active_status", 24 | "breakpoints": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc" 2 | import { dirname } from "path" 3 | import { fileURLToPath } from "url" 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }) 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | rules: { 16 | "react/no-unescaped-entities": "off", 17 | "@typescript-eslint/no-unused-vars": "off", 18 | "@next/next/no-img-element": "off" 19 | } 20 | } 21 | ] 22 | 23 | export default eslintConfig 24 | -------------------------------------------------------------------------------- /hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /app/(unauthenticated)/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "./_components/footer" 2 | import { HeaderWrapper } from "./_components/header-wrapper" 3 | import { ScrollIndicator } from "./_components/scroll-indicator" 4 | import { SiteBanner } from "./_components/site-banner" 5 | import { StickyCTA } from "./_components/sticky-cta" 6 | 7 | export default async function MarketingLayout({ 8 | children 9 | }: { 10 | children: React.ReactNode 11 | }) { 12 | return ( 13 | <> 14 | 15 | 16 | {children} 17 |