├── .idea
├── .gitignore
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
├── supabase-nextjs-template.iml
└── vcs.xml
├── LICENSE
├── README.md
├── nextjs
├── .env.template
├── .gitignore
├── .idea
│ ├── .gitignore
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── modules.xml
│ ├── nextjs.iml
│ └── vcs.xml
├── README.md
├── components.json
├── eslint.config.mjs
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── terms
│ │ ├── privacy-notice.md
│ │ ├── refund-policy.md
│ │ └── terms-of-service.md
│ ├── vercel.svg
│ └── window.svg
├── src
│ ├── app
│ │ ├── api
│ │ │ └── auth
│ │ │ │ └── callback
│ │ │ │ └── route.ts
│ │ ├── app
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ ├── storage
│ │ │ │ └── page.tsx
│ │ │ ├── table
│ │ │ │ └── page.tsx
│ │ │ └── user-settings
│ │ │ │ └── page.tsx
│ │ ├── auth
│ │ │ ├── 2fa
│ │ │ │ └── page.tsx
│ │ │ ├── forgot-password
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── login
│ │ │ │ └── page.tsx
│ │ │ ├── register
│ │ │ │ └── page.tsx
│ │ │ ├── reset-password
│ │ │ │ └── page.tsx
│ │ │ └── verify-email
│ │ │ │ └── page.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── legal
│ │ │ ├── [document]
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── components
│ │ ├── AppLayout.tsx
│ │ ├── AuthAwareButtons.tsx
│ │ ├── Confetti.tsx
│ │ ├── Cookies.tsx
│ │ ├── HomePricing.tsx
│ │ ├── LegalDocument.tsx
│ │ ├── LegalDocuments.tsx
│ │ ├── MFASetup.tsx
│ │ ├── MFAVerification.tsx
│ │ ├── SSOButtons.tsx
│ │ └── ui
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── input.tsx
│ │ │ └── textarea.tsx
│ ├── lib
│ │ ├── context
│ │ │ └── GlobalContext.tsx
│ │ ├── pricing.ts
│ │ ├── supabase
│ │ │ ├── client.ts
│ │ │ ├── middleware.ts
│ │ │ ├── server.ts
│ │ │ ├── serverAdminClient.ts
│ │ │ └── unified.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ └── middleware.ts
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
└── supabase
├── .gitignore
├── config.toml
├── migrations
├── 20250107210416_MFA.sql
├── 20250130165844_example_storage.sql
├── 20250130181110_storage_policies.sql
└── 20250130181641_todo_list.sql
└── migrations_for_old
├── 20250107210416_MFA.sql
├── 20250130165844_example_storage.sql
├── 20250130181110_storage_policies.sql
├── 20250130181641_todo_list.sql
└── 20250525183944_auth_removal.sql
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/supabase-nextjs-template.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Supabase Next.js SaaS Template
2 |
3 | A production-ready SaaS template built with Next.js 15, Supabase, and Tailwind CSS. This template provides everything you need to quickly launch your SaaS product, including authentication, user management, file storage, and more.
4 |
5 | ## LIVE DEMO
6 |
7 | Demo is here - https://basicsass.razikus.com
8 |
9 |
10 | ## Deployment video
11 |
12 | Video is here - https://www.youtube.com/watch?v=kzbXavLndmE
13 |
14 | ## Migration from auth schema
15 |
16 | According to this - https://github.com/Razikus/supabase-nextjs-template/issues/4
17 |
18 | We are no longer able to modify auth schema. I modified original migrations to rename it to custom schema. If you need to migrate from older version - check supabase/migrations_for_old/20250525183944_auth_removal.sql
19 |
20 | ## SupaNuggets
21 |
22 | On top of this template I'm building a SupaNuggets series - 50 mini apps
23 |
24 | https://supanuggets.razikus.com - grab your copy for free :) (Pay As You Want model)
25 |
26 | ## 🚀 Features
27 |
28 | - **Authentication**
29 | - Email/Password authentication
30 | - Multi-factor authentication (MFA) support
31 | - OAuth/SSO integration ready
32 | - Password reset and email verification
33 |
34 | - **User Management**
35 | - User profiles and settings
36 | - Secure password management
37 | - Session handling
38 |
39 | - **File Management Demo (2FA ready)**
40 | - Secure file upload and storage
41 | - File sharing capabilities
42 | - Drag-and-drop interface
43 | - Progress tracking
44 |
45 | - **Task Management Demo (2FA ready)**
46 | - CRUD operations example
47 | - Real-time updates
48 | - Filtering and sorting
49 | - Row-level security
50 |
51 | - **Security**
52 | - Row Level Security (RLS) policies
53 | - Secure file storage policies
54 | - Protected API routes
55 | - MFA implementation
56 |
57 | - **UI/UX**
58 | - Modern, responsive design
59 | - Dark mode support
60 | - Loading states
61 | - Error handling
62 | - Toast notifications
63 | - Confetti animations
64 |
65 | - **Legal & Compliance**
66 | - Privacy Policy template
67 | - Terms of Service template
68 | - Refund Policy template
69 | - GDPR-ready cookie consent
70 |
71 | ## 🛠️ Tech Stack
72 |
73 | - **Frontend**
74 | - Next.js 15 (App Router)
75 | - React 19
76 | - Tailwind CSS
77 | - shadcn/ui components
78 | - Lucide icons
79 |
80 | - **Backend**
81 | - Supabase
82 | - PostgreSQL
83 | - Row Level Security
84 | - Storage Buckets
85 |
86 | - **Authentication**
87 | - Supabase Auth
88 | - MFA support
89 | - OAuth providers
90 |
91 | ## 📦 Getting Started - local dev
92 |
93 | 1. Fork or clone repository
94 | 2. Prepare Supabase Project URL (Project URL from `Project Settings` -> `API` -> `Project URL`)
95 | 3. Prepare Supabase Anon and Service Key (`Anon Key`, `Service Key` from `Project Settings` -> `API` -> `anon public` and `service_role`)
96 | 4. Prepare Supabase Database Password (You can reset it inside `Project Settings` -> `Database` -> `Database Password`)
97 | 5. If you already know your app url -> adjust supabase/config.toml `site_url` and `additional_redirect_urls`, you can do it later
98 | 6. Run following commands (inside root of forked / downloaded repository):
99 |
100 | ```bash
101 | # Login to supabase
102 | npx supabase login
103 | # Link project to supabase (require database password) - you will get selector prompt
104 | npx supabase link
105 |
106 | # Send config to the server - may require confirmation (y)
107 | npx supabase config push
108 |
109 | # Up migrations
110 | npx supabase migrations up --linked
111 |
112 | ```
113 |
114 | 7. Go to next/js folder and run `yarn`
115 | 8. Copy .env.template to .env.local
116 | 9. Adjust .env.local
117 | ```
118 | NEXT_PUBLIC_SUPABASE_URL=https://APIURL
119 | NEXT_PUBLIC_SUPABASE_ANON_KEY=ANONKEY
120 | PRIVATE_SUPABASE_SERVICE_KEY=SERVICEROLEKEY
121 |
122 | ```
123 | 10. Run yarn dev
124 | 11. Go to http://localhost:3000 🎉
125 |
126 | ## 🚀 Getting Started - deploy to vercel
127 |
128 | 1. Fork or clone repository
129 | 2. Create project in Vercel - choose your repo
130 | 3. Paste content of .env.local into environment variables
131 | 4. Click deploy
132 | 5. Adjust in supabase/config.toml site_url and additional_redirect_urls (important in additional_redirect_urls is to have https://YOURURL/** - these 2 **)
133 | 6. Done!
134 |
135 | ## 📄 Legal Documents
136 |
137 | The template includes customizable legal documents - these are in markdown, so you can adjust them as you see fit:
138 |
139 | - Privacy Policy (`/public/terms/privacy-notice.md`)
140 | - Terms of Service (`/public/terms/terms-of-service.md`)
141 | - Refund Policy (`/public/terms/refund-policy.md`)
142 |
143 | ## 🎨 Theming
144 |
145 | The template includes several pre-built themes:
146 | - `theme-sass` (Default)
147 | - `theme-blue`
148 | - `theme-purple`
149 | - `theme-green`
150 |
151 | Change the theme by updating the `NEXT_PUBLIC_THEME` environment variable.
152 |
153 | ## 🤝 Contributing
154 |
155 | Contributions are welcome! Please feel free to submit a Pull Request.
156 |
157 |
158 | ## Need Multitenancy, Billing (Paddle) and Role Based Access Control?
159 |
160 | I have paid template as well available here:
161 |
162 | https://sasstemplate.razikus.com
163 |
164 | Basically it's the same template but with Paddle + organisations API keys + multiple organisations + Role Based Access Control
165 |
166 | For code GITHUB you can get -50% off
167 |
168 | https://razikus.gumroad.com/l/supatemplate/GITHUB
169 |
170 | ## 📝 License
171 |
172 | This project is licensed under the Apache License - see the LICENSE file for details.
173 |
174 | ## 💪 Support
175 |
176 | If you find this template helpful, please consider giving it a star ⭐️
177 |
178 | Or buy me a coffee!
179 |
180 | - [BuyMeACoffee](https://buymeacoffee.com/razikus)
181 |
182 | My socials:
183 |
184 | - [Twitter](https://twitter.com/Razikus_)
185 | - [GitHub](https://github.com/Razikus)
186 | - [Website](https://www.razikus.com)
187 |
188 |
189 | ## 🙏 Acknowledgments
190 |
191 | - [Next.js](https://nextjs.org/)
192 | - [Supabase](https://supabase.com/)
193 | - [Tailwind CSS](https://tailwindcss.com/)
194 | - [shadcn/ui](https://ui.shadcn.com/)
195 |
--------------------------------------------------------------------------------
/nextjs/.env.template:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_SUPABASE_URL=https://YOURSUPABASE.supabase.co
2 | NEXT_PUBLIC_SUPABASE_ANON_KEY=YYY
3 | PRIVATE_SUPABASE_SERVICE_KEY=XXX
4 | NEXT_PUBLIC_PRODUCTNAME=BasicSass
5 | NEXT_PUBLIC_SSO_PROVIDERS=github,google,facebook,apple
6 |
7 |
8 | NEXT_PUBLIC_GOOGLE_TAG=G-GOOGLETAG
9 | NEXT_PUBLIC_THEME=theme-sass
10 |
11 |
12 | NEXT_PUBLIC_TIERS_NAMES=Basic,Growth,Max
13 | NEXT_PUBLIC_TIERS_PRICES=99,199,299
14 | NEXT_PUBLIC_TIERS_DESCRIPTIONS=Perfect for getting started,Best for growing teams,For enterprise-grade needs
15 | NEXT_PUBLIC_TIERS_FEATURES=14 day free trial|30 PDF files,14 day free trial|1000 PDF files,14 day free trial|Unlimited PDF files
16 | NEXT_PUBLIC_POPULAR_TIER=Growth
17 | NEXT_PUBLIC_COMMON_FEATURES=SSL security,unlimited updates,premium support
--------------------------------------------------------------------------------
/nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/nextjs/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/nextjs/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/nextjs/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/nextjs/.idea/nextjs.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/nextjs/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/nextjs/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/nextjs/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/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 | }
--------------------------------------------------------------------------------
/nextjs/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
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 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/nextjs/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sasstemplate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next/third-parties": "^15.1.5",
13 | "@paddle/paddle-js": "^1.3.3",
14 | "@paddle/paddle-node-sdk": "^2.3.2",
15 | "@radix-ui/react-alert-dialog": "^1.1.5",
16 | "@radix-ui/react-dialog": "^1.1.5",
17 | "@radix-ui/react-slot": "^1.1.1",
18 | "@supabase/ssr": "^0.5.2",
19 | "@supabase/supabase-js": "^2.47.10",
20 | "@vercel/analytics": "^1.4.1",
21 | "class-variance-authority": "^0.7.1",
22 | "clsx": "^2.1.1",
23 | "cookies-next": "^5.0.2",
24 | "lucide-react": "^0.469.0",
25 | "next": "15.1.3",
26 | "react": "^19.0.0",
27 | "react-dom": "^19.0.0",
28 | "react-markdown": "^9.0.3",
29 | "recharts": "^2.15.0",
30 | "tailwind-merge": "^2.6.0",
31 | "tailwindcss-animate": "^1.0.7"
32 | },
33 | "devDependencies": {
34 | "@eslint/eslintrc": "^3",
35 | "@types/node": "^20",
36 | "@types/react": "^19",
37 | "@types/react-dom": "^19",
38 | "eslint": "^9",
39 | "eslint-config-next": "15.1.3",
40 | "postcss": "^8",
41 | "tailwindcss": "^3.4.1",
42 | "typescript": "^5"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/nextjs/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/nextjs/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nextjs/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nextjs/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nextjs/public/terms/privacy-notice.md:
--------------------------------------------------------------------------------
1 | # Privacy Notice
2 |
3 | Last Updated: December 14, 2024
4 |
5 | ## 1. Company Information
6 |
7 | This Privacy Notice applies to:
8 | Company Name:
9 |
10 | Tax number: 1234567890
11 |
12 | ## 2. Introduction
13 |
14 | This Privacy Notice explains how we collect, use, disclose, and safeguard your information when you use SupaSasS services.
15 |
16 | ## 3. Information We Collect
17 |
18 | ### 3.1 Personal Information
19 | - Email address
20 | - Account credentials
21 | - Payment information (processed by Paddle)
22 | - Usage data
23 |
24 | ### 3.2 Business Information
25 | - Shopify store data (when using analysis tools)
26 | - Domain search queries
27 | - Analytics data
28 |
29 | ### 3.3 Technical Information
30 | - IP address
31 | - Browser type
32 | - Device information
33 | - Cookies and similar technologies
34 |
35 | ## 4. How We Use Your Information
36 |
37 | We use collected information for:
38 | - Providing and maintaining our services
39 | - Processing payments
40 | - Improving our services
41 | - Communicating with you
42 | - Analyzing usage patterns
43 | - Preventing fraud
44 |
45 | ## 5. Data Storage and Security
46 |
47 | ### 5.1 Storage
48 | - Data is stored on secure servers
49 | - We use Supabase for database management
50 | - Payment data is handled by Paddle
51 |
52 | ### 5.2 Security Measures
53 | - Encryption in transit and at rest
54 | - Regular security audits
55 | - Access controls and monitoring
56 |
57 | ## 6. Data Sharing and Disclosure
58 |
59 | We may share information with:
60 | - Service providers (Paddle, Supabase)
61 | - Legal authorities when required
62 | - Business partners with your consent
63 |
64 | ## 7. Your Rights
65 |
66 | You have the right to:
67 | - Access your personal data
68 | - Correct inaccurate data
69 | - Request data deletion
70 | - Object to processing
71 | - Data portability
72 |
73 | ## 8. Cookies and Tracking
74 |
75 | We use cookies for:
76 | - Authentication
77 | - Preferences
78 | - Analytics
79 | - Marketing (with consent)
80 |
81 | ## 9. International Data Transfers
82 |
83 | - Data may be processed in various locations
84 | - We ensure appropriate safeguards
85 | - EU data protection standards applied
86 |
87 | ## 10. Children's Privacy
88 |
89 | - Services not intended for users under 18
90 | - We do not knowingly collect children's data
91 | - Parents may request data deletion
92 |
93 | ## 11. Changes to Privacy Notice
94 |
95 | - We may update this notice periodically
96 | - Changes effective upon posting
97 | - Material changes notified via email
98 |
99 | ## 12. Contact Information
100 |
101 | For privacy-related questions:
102 | Email: contact@supasass.com
103 |
104 |
--------------------------------------------------------------------------------
/nextjs/public/terms/refund-policy.md:
--------------------------------------------------------------------------------
1 | # Refund Policy
2 |
3 | Last Updated: December 14, 2024
4 |
5 | ## 1. Company Information
6 |
7 | Company Name:
8 |
9 | Tax number: 1234567890
10 |
11 | ## 2. General Policy
12 |
13 | We strive to ensure customer satisfaction with all our digital services and products. This policy outlines our procedures for refunds and cancellations.
14 |
15 | ## 3. Subscription Services
16 |
17 | ### 3.1 Cancellation
18 | - You may cancel your subscription at any time
19 | - Access continues until the end of the billing period
20 | - No partial refunds for unused time
21 |
22 | ### 3.2 Automatic Renewals
23 | - Subscriptions renew automatically
24 | - Cancel at least 24 hours before renewal
25 | - No refunds for automatic renewals
26 |
27 | ## 4. Data Products
28 |
29 | ### 4.1 Digital Products
30 | - Data products are non-refundable once delivered
31 | - Sample data available before purchase
32 | - Technical support provided for access issues
33 |
34 | ### 4.2 Custom Data Solutions
35 | - Custom orders are non-refundable
36 | - Requirements confirmed before processing
37 | - Revision period available
38 |
39 | ## 5. Refund Eligibility
40 |
41 | Refunds may be considered for:
42 | - Technical issues preventing access
43 | - Service unavailability
44 | - Incorrect charges
45 | - Legal requirements
46 |
47 | ## 6. Refund Process
48 |
49 | To request a refund:
50 | 1. Contact support within 14 days
51 | 2. Provide order details
52 | 3. Explain refund reason
53 | 4. Allow 5-10 business days for processing
54 |
55 | ## 7. Payment Processing
56 |
57 | ### 7.1 Refund Method
58 | - Refunds processed through original payment method
59 | - Processed via Paddle
60 | - Currency same as original payment
61 |
62 | ### 7.2 Processing Time
63 | - 2-5 business days for approval
64 | - 5-10 business days for bank processing
65 | - May vary by payment method
66 |
67 | ## 8. Exceptions
68 |
69 | No refunds for:
70 | - Used or accessed digital products
71 | - Expired subscriptions
72 | - Violations of Terms of Service
73 | - Fraudulent activities
74 |
75 | ## 9. Customer Support
76 |
77 | For refund requests contact:
78 | Email: contact@komiru.com
79 |
80 | ## 10. Changes to Policy
81 |
82 | - We reserve the right to modify this policy
83 | - Changes effective upon posting
84 | - Current customers notified of material changes
85 |
86 |
--------------------------------------------------------------------------------
/nextjs/public/terms/terms-of-service.md:
--------------------------------------------------------------------------------
1 | # Terms of Service
2 |
3 | Last Updated: December 14, 2024
4 |
5 | ## 1. Company Information
6 |
7 | Company Name:
8 |
9 | Tax number: 1234567890
10 |
11 | ## 2. Introduction
12 |
13 | Welcome to SupaSasS ("we," "our," or "us"). These Terms of Service ("Terms") govern your access to and use of our website, applications, and services (collectively, the "Services"), including AI-powered tools, data analysis features, and subscription services.
14 |
15 | ## 3. Acceptance of Terms
16 |
17 | By accessing or using our Services, you agree to be bound by these Terms. If you disagree with any part of these terms, you may not access the Services.
18 |
19 | ## 4. Services Description
20 |
21 | Our Services include:
22 | - AI-powered domain search tools
23 | - Shopify store analysis
24 | - Data marketplace access
25 | - Customer analytics tools
26 | - Multi-language support features
27 |
28 | ## 5. Account Registration and Security
29 |
30 | ### 5.1 Account Creation
31 | - You must register for an account to access most features
32 | - You must provide accurate, current, and complete information
33 | - You must be at least 18 years old to create an account
34 |
35 | ### 5.2 Account Security
36 | - You are responsible for maintaining the confidentiality of your account credentials
37 | - You must notify us immediately of any unauthorized access
38 | - We reserve the right to disable accounts that violate our terms
39 |
40 | ## 6. Subscription Terms
41 |
42 | ### 6.1 Billing
43 | - Subscription fees are billed in advance through Paddle
44 | - Prices are listed in EUR/USD and may be subject to local taxes
45 | - We reserve the right to change pricing with 30 days notice
46 |
47 | ### 6.2 Cancellation
48 | - You may cancel your subscription at any time through your account dashboard
49 | - Refunds are subject to our Refund Policy
50 | - Cancellation will take effect at the end of the current billing period
51 |
52 | ## 7. Data Usage and Privacy
53 |
54 | ### 7.1 Data Collection
55 | - We collect and process data as described in our Privacy Policy
56 | - You retain ownership of your content and data
57 | - We use data to improve and maintain our services
58 |
59 | ### 7.2 AI Analysis
60 | - Our AI tools analyze data you provide
61 | - Analysis results are for informational purposes only
62 | - We do not guarantee accuracy of AI-generated recommendations
63 |
64 | ## 8. Intellectual Property
65 |
66 | ### 8.1 Our Rights
67 | - All content, features, and functionality are owned by Adam Raźniewski oprogramowanie
68 | - Our trademarks and trade dress may not be used without written permission
69 |
70 | ### 8.2 Your Rights
71 | - You retain all rights to data you upload
72 | - You grant us license to use your data to provide services
73 |
74 | ## 9. Limitations of Liability
75 |
76 | - Services are provided "as is" without warranties
77 | - We are not liable for any indirect, incidental, or consequential damages
78 | - Our liability is limited to the amount paid for services in the past 12 months
79 |
80 | ## 10. Termination
81 |
82 | We reserve the right to terminate or suspend access to our Services:
83 | - For violations of these Terms
84 | - For fraudulent or illegal activities
85 | - At our sole discretion with reasonable notice
86 |
87 | ## 11. Changes to Terms
88 |
89 | We may modify these Terms at any time:
90 | - Changes will be effective upon posting
91 | - Continued use constitutes acceptance of changes
92 | - Material changes will be notified via email
93 |
94 | ## 12. Governing Law
95 |
96 | These Terms shall be governed by and construed in accordance with the laws of Poland, without regard to its conflict of law provisions.
97 |
98 | ## 13. Contact Information
99 |
100 | For any questions about these Terms, please contact us at:
101 | Email: contact@supasass.com
102 |
103 |
--------------------------------------------------------------------------------
/nextjs/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nextjs/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nextjs/src/app/api/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | // src/app/api/auth/callback/route.ts
2 | import { NextResponse } from 'next/server'
3 | import { createSSRSassClient } from "@/lib/supabase/server";
4 |
5 | export async function GET(request: Request) {
6 | const requestUrl = new URL(request.url)
7 | const code = requestUrl.searchParams.get('code')
8 |
9 | if (code) {
10 | const supabase = await createSSRSassClient()
11 | const client = supabase.getSupabaseClient()
12 |
13 | // Exchange the code for a session
14 | await supabase.exchangeCodeForSession(code)
15 |
16 | // Check MFA status
17 | const { data: aal, error: aalError } = await client.auth.mfa.getAuthenticatorAssuranceLevel()
18 |
19 | if (aalError) {
20 | console.error('Error checking MFA status:', aalError)
21 | return NextResponse.redirect(new URL('/auth/login', request.url))
22 | }
23 |
24 | // If user needs to complete MFA verification
25 | if (aal.nextLevel === 'aal2' && aal.nextLevel !== aal.currentLevel) {
26 | return NextResponse.redirect(new URL('/auth/2fa', request.url))
27 | }
28 |
29 | // If MFA is not required or already verified, proceed to app
30 | return NextResponse.redirect(new URL('/app', request.url))
31 | }
32 |
33 | // If no code provided, redirect to login
34 | return NextResponse.redirect(new URL('/auth/login', request.url))
35 | }
--------------------------------------------------------------------------------
/nextjs/src/app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | // src/app/app/layout.tsx
2 | import AppLayout from '@/components/AppLayout';
3 | import { GlobalProvider } from '@/lib/context/GlobalContext';
4 |
5 | export default function Layout({ children }: { children: React.ReactNode }) {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
--------------------------------------------------------------------------------
/nextjs/src/app/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from 'react';
3 | import { useGlobal } from '@/lib/context/GlobalContext';
4 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
5 | import { CalendarDays, Settings, ExternalLink } from 'lucide-react';
6 | import Link from 'next/link';
7 |
8 | export default function DashboardContent() {
9 | const { loading, user } = useGlobal();
10 |
11 | const getDaysSinceRegistration = () => {
12 | if (!user?.registered_at) return 0;
13 | const today = new Date();
14 | const diffTime = Math.abs(today.getTime() - user.registered_at.getTime());
15 | return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
16 | };
17 |
18 | if (loading) {
19 | return (
20 |
23 | );
24 | }
25 |
26 | const daysSinceRegistration = getDaysSinceRegistration();
27 |
28 | return (
29 |
30 |
31 |
32 | Welcome, {user?.email?.split('@')[0]}! 👋
33 |
34 |
35 | Member for {daysSinceRegistration} days
36 |
37 |
38 |
39 |
40 | {/* Quick Actions */}
41 |
42 |
43 | Quick Actions
44 | Frequently used features
45 |
46 |
47 |
48 |
52 |
53 |
54 |
55 |
56 |
User Settings
57 |
Manage your account preferences
58 |
59 |
60 |
61 |
65 |
66 |
67 |
68 |
69 |
Example Page
70 |
Check out example features
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
--------------------------------------------------------------------------------
/nextjs/src/app/app/user-settings/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from 'react';
3 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
4 | import { Alert, AlertDescription } from '@/components/ui/alert';
5 | import { useGlobal } from '@/lib/context/GlobalContext';
6 | import { createSPASassClient } from '@/lib/supabase/client';
7 | import { Key, User, CheckCircle } from 'lucide-react';
8 | import { MFASetup } from '@/components/MFASetup';
9 |
10 | export default function UserSettingsPage() {
11 | const { user } = useGlobal();
12 | const [newPassword, setNewPassword] = useState('');
13 | const [confirmPassword, setConfirmPassword] = useState('');
14 | const [loading, setLoading] = useState(false);
15 | const [error, setError] = useState('');
16 | const [success, setSuccess] = useState('');
17 |
18 |
19 |
20 | const handlePasswordChange = async (e: React.FormEvent) => {
21 | e.preventDefault();
22 | if (newPassword !== confirmPassword) {
23 | setError("New passwords don't match");
24 | return;
25 | }
26 |
27 | setLoading(true);
28 | setError('');
29 | setSuccess('');
30 |
31 | try {
32 | const supabase = await createSPASassClient();
33 | const client = supabase.getSupabaseClient();
34 |
35 | const { error } = await client.auth.updateUser({
36 | password: newPassword
37 | });
38 |
39 | if (error) throw error;
40 |
41 | setSuccess('Password updated successfully');
42 | setNewPassword('');
43 | setConfirmPassword('');
44 | } catch (err: Error | unknown) {
45 | if (err instanceof Error) {
46 | console.error('Error updating password:', err);
47 | setError(err.message);
48 | } else {
49 | console.error('Error updating password:', err);
50 | setError('Failed to update password');
51 | }
52 | } finally {
53 | setLoading(false);
54 | }
55 | };
56 |
57 |
58 |
59 | return (
60 |
61 |
62 |
User Settings
63 |
64 | Manage your account settings and preferences
65 |
66 |
67 |
68 | {error && (
69 |
70 | {error}
71 |
72 | )}
73 |
74 | {success && (
75 |
76 |
77 | {success}
78 |
79 | )}
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | User Details
88 |
89 | Your account information
90 |
91 |
92 |
93 |
User ID
94 |
{user?.id}
95 |
96 |
97 |
Email
98 |
{user?.email}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | Change Password
108 |
109 | Update your account password
110 |
111 |
112 |
147 |
148 |
149 |
150 |
{
152 | setSuccess('Two-factor authentication settings updated successfully');
153 | }}
154 | />
155 |
156 |
157 |
158 | );
159 | }
--------------------------------------------------------------------------------
/nextjs/src/app/auth/2fa/page.tsx:
--------------------------------------------------------------------------------
1 | // src/app/auth/2fa/page.tsx
2 | 'use client';
3 |
4 | import { useState, useEffect } from 'react';
5 | import { useRouter } from 'next/navigation';
6 | import { createSPASassClient } from '@/lib/supabase/client';
7 | import { MFAVerification } from '@/components/MFAVerification';
8 |
9 | export default function TwoFactorAuthPage() {
10 | const router = useRouter();
11 | const [loading, setLoading] = useState(true);
12 | const [error, setError] = useState('');
13 |
14 | useEffect(() => {
15 | checkMFAStatus();
16 | }, []);
17 |
18 | const checkMFAStatus = async () => {
19 | try {
20 | const supabase = await createSPASassClient();
21 | const client = supabase.getSupabaseClient();
22 |
23 | const { data: { user }, error: sessionError } = await client.auth.getUser();
24 | if (sessionError || !user) {
25 | router.push('/auth/login');
26 | return;
27 | }
28 |
29 | const { data: aal, error: aalError } = await client.auth.mfa.getAuthenticatorAssuranceLevel();
30 |
31 | if (aalError) throw aalError;
32 |
33 | if (aal.currentLevel === 'aal2' || aal.nextLevel === 'aal1') {
34 | router.push('/app');
35 | return;
36 | }
37 |
38 | setLoading(false);
39 | } catch (err) {
40 | setError(err instanceof Error ? err.message : 'An error occurred');
41 | setLoading(false);
42 | }
43 | };
44 |
45 | const handleVerified = () => {
46 | router.push('/app');
47 | };
48 |
49 | if (loading) {
50 | return (
51 |
54 | );
55 | }
56 |
57 | if (error) {
58 | return (
59 |
62 | );
63 | }
64 |
65 | return (
66 |
67 |
68 |
69 | );
70 | }
--------------------------------------------------------------------------------
/nextjs/src/app/auth/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { createSPASassClient } from '@/lib/supabase/client';
5 | import Link from 'next/link';
6 | import { CheckCircle } from 'lucide-react';
7 |
8 | export default function ForgotPasswordPage() {
9 | const [email, setEmail] = useState('');
10 | const [error, setError] = useState('');
11 | const [loading, setLoading] = useState(false);
12 | const [success, setSuccess] = useState(false);
13 |
14 | const handleSubmit = async (e: React.FormEvent) => {
15 | e.preventDefault();
16 | setError('');
17 | setLoading(true);
18 |
19 | try {
20 | const supabase = await createSPASassClient();
21 | const { error } = await supabase.getSupabaseClient().auth.resetPasswordForEmail(email, {
22 | redirectTo: `${window.location.origin}/auth/reset-password`,
23 | });
24 |
25 | if (error) throw error;
26 |
27 | setSuccess(true);
28 | } catch (err) {
29 | if (err instanceof Error) {
30 | setError(err.message);
31 | } else {
32 | setError('An unknown error occurred');
33 | }
34 | } finally {
35 | setLoading(false);
36 | }
37 | };
38 |
39 | if (success) {
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Check your email
49 |
50 |
51 |
52 | We have sent a password reset link to your email address.
53 | Please check your inbox and follow the instructions to reset your password.
54 |
55 |
56 |
57 |
58 | Return to login
59 |
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | return (
67 |
68 |
69 |
70 | Reset your password
71 |
72 |
73 |
74 | {error && (
75 |
76 | {error}
77 |
78 | )}
79 |
80 |
112 |
113 |
114 | Remember your password?
115 | {' '}
116 |
117 | Sign in
118 |
119 |
120 |
121 | );
122 | }
--------------------------------------------------------------------------------
/nextjs/src/app/auth/layout.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { ArrowLeft } from 'lucide-react';
3 |
4 | export default function AuthLayout({
5 | children,
6 | }: {
7 | children: React.ReactNode;
8 | }) {
9 | const productName = process.env.NEXT_PUBLIC_PRODUCTNAME;
10 | const testimonials = [
11 | {
12 | quote: "This template helped us launch our SaaS product in just two weeks. The authentication and multi-tenancy features are rock solid.",
13 | author: "Sarah Chen",
14 | role: "CTO, TechStart",
15 | avatar: "SC"
16 | },
17 | {
18 | quote: "The best part is how well thought out the organization management is. It saved us months of development time.",
19 | author: "Michael Roberts",
20 | role: "Founder, DataFlow",
21 | avatar: "MR"
22 | },
23 | {
24 | quote: "Clean code, great documentation, and excellent support. Exactly what we needed to get our MVP off the ground.",
25 | author: "Jessica Kim",
26 | role: "Lead Developer, CloudScale",
27 | avatar: "JK"
28 | }
29 | ];
30 |
31 | return (
32 |
33 |
34 |
38 |
39 | Back to Homepage
40 |
41 |
42 |
43 |
44 | {productName}
45 |
46 |
47 |
48 |
49 | {children}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Trusted by developers worldwide
58 |
59 | {testimonials.map((testimonial, index) => (
60 |
64 |
65 |
66 |
67 | {testimonial.avatar}
68 |
69 |
70 |
71 |
72 | "{testimonial.quote}"
73 |
74 |
75 |
76 | {testimonial.author}
77 |
78 |
79 | {testimonial.role}
80 |
81 |
82 |
83 |
84 |
85 | ))}
86 |
87 |
88 | Join thousands of developers building with {productName}
89 |
90 |
91 |
92 |
93 |
94 |
95 | );
96 | }
--------------------------------------------------------------------------------
/nextjs/src/app/auth/login/page.tsx:
--------------------------------------------------------------------------------
1 | // src/app/auth/login/page.tsx
2 | 'use client';
3 |
4 | import { createSPASassClient } from '@/lib/supabase/client';
5 | import {useEffect, useState} from 'react';
6 | import { useRouter } from 'next/navigation';
7 | import Link from 'next/link';
8 | import SSOButtons from '@/components/SSOButtons';
9 |
10 | export default function LoginPage() {
11 | const [email, setEmail] = useState('');
12 | const [password, setPassword] = useState('');
13 | const [error, setError] = useState('');
14 | const [loading, setLoading] = useState(false);
15 | const [showMFAPrompt, setShowMFAPrompt] = useState(false);
16 | const router = useRouter();
17 |
18 | const handleSubmit = async (e: React.FormEvent) => {
19 | e.preventDefault();
20 | setError('');
21 | setLoading(true);
22 |
23 | try {
24 | const client = await createSPASassClient();
25 | const { error: signInError } = await client.loginEmail(email, password);
26 |
27 | if (signInError) throw signInError;
28 |
29 | // Check if MFA is required
30 | const supabase = client.getSupabaseClient();
31 | const { data: mfaData, error: mfaError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
32 |
33 | if (mfaError) throw mfaError;
34 |
35 | if (mfaData.nextLevel === 'aal2' && mfaData.nextLevel !== mfaData.currentLevel) {
36 | setShowMFAPrompt(true);
37 | } else {
38 | router.push('/app');
39 | return;
40 | }
41 | } catch (err) {
42 | if (err instanceof Error) {
43 | setError(err.message);
44 | } else {
45 | setError('An unknown error occurred');
46 | }
47 | } finally {
48 | setLoading(false);
49 | }
50 | };
51 |
52 |
53 | useEffect(() => {
54 | if(showMFAPrompt) {
55 | router.push('/auth/2fa');
56 | }
57 | }, [showMFAPrompt, router]);
58 |
59 |
60 | return (
61 |
62 | {error && (
63 |
64 | {error}
65 |
66 | )}
67 |
68 |
123 |
124 |
125 |
126 |
127 | Don't have an account?
128 | {' '}
129 |
130 | Sign up
131 |
132 |
133 |
134 | );
135 | }
--------------------------------------------------------------------------------
/nextjs/src/app/auth/register/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {createSPASassClient} from '@/lib/supabase/client';
4 | import { useState } from 'react';
5 | import { useRouter } from 'next/navigation';
6 | import Link from 'next/link';
7 | import SSOButtons from "@/components/SSOButtons";
8 |
9 | export default function RegisterPage() {
10 | const [email, setEmail] = useState('');
11 | const [password, setPassword] = useState('');
12 | const [confirmPassword, setConfirmPassword] = useState('');
13 | const [error, setError] = useState('');
14 | const [loading, setLoading] = useState(false);
15 | const [acceptedTerms, setAcceptedTerms] = useState(false);
16 | const router = useRouter();
17 |
18 | const handleSubmit = async (e: React.FormEvent) => {
19 | e.preventDefault();
20 | setError('');
21 |
22 | if (!acceptedTerms) {
23 | setError('You must accept the Terms of Service and Privacy Policy');
24 | return;
25 | }
26 |
27 | if (password !== confirmPassword) {
28 | setError("Passwords don't match");
29 | return;
30 | }
31 |
32 | setLoading(true);
33 |
34 | try {
35 | const supabase = await createSPASassClient();
36 | const { error } = await supabase.registerEmail(email, password);
37 |
38 | if (error) throw error;
39 |
40 | router.push('/auth/verify-email');
41 | } catch (err: Error | unknown) {
42 | if(err instanceof Error) {
43 | setError(err.message);
44 | } else {
45 | setError('An unknown error occurred');
46 | }
47 | } finally {
48 | setLoading(false);
49 | }
50 | };
51 |
52 | return (
53 |
54 | {error && (
55 |
56 | {error}
57 |
58 | )}
59 |
60 |
159 |
160 |
161 |
162 |
163 | Already have an account?
164 | {' '}
165 |
166 | Sign in
167 |
168 |
169 |
170 | );
171 | }
--------------------------------------------------------------------------------
/nextjs/src/app/auth/reset-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect } from 'react';
4 | import { createSPASassClient } from '@/lib/supabase/client';
5 | import { useRouter } from 'next/navigation';
6 | import { CheckCircle, Key } from 'lucide-react';
7 |
8 | export default function ResetPasswordPage() {
9 | const [newPassword, setNewPassword] = useState('');
10 | const [confirmPassword, setConfirmPassword] = useState('');
11 | const [error, setError] = useState('');
12 | const [loading, setLoading] = useState(false);
13 | const [success, setSuccess] = useState(false);
14 | const router = useRouter();
15 |
16 | // Check if we have a valid recovery session
17 | useEffect(() => {
18 | const checkSession = async () => {
19 | try {
20 | const supabase = await createSPASassClient();
21 | const { data: { user }, error } = await supabase.getSupabaseClient().auth.getUser();
22 |
23 | if (error || !user) {
24 | setError('Invalid or expired reset link. Please request a new password reset.');
25 | }
26 | } catch {
27 | setError('Failed to verify reset session');
28 | }
29 | };
30 |
31 | checkSession();
32 | }, []);
33 |
34 | const handleSubmit = async (e: React.FormEvent) => {
35 | e.preventDefault();
36 | setError('');
37 |
38 | if (newPassword !== confirmPassword) {
39 | setError("Passwords don't match");
40 | return;
41 | }
42 |
43 | if (newPassword.length < 6) {
44 | setError('Password must be at least 6 characters long');
45 | return;
46 | }
47 |
48 | setLoading(true);
49 |
50 | try {
51 | const supabase = await createSPASassClient();
52 | const { error } = await supabase.getSupabaseClient().auth.updateUser({
53 | password: newPassword
54 | });
55 |
56 | if (error) throw error;
57 |
58 | setSuccess(true);
59 | setTimeout(() => {
60 | router.push('/app');
61 | }, 3000);
62 | } catch (err) {
63 | if (err instanceof Error) {
64 | setError(err.message);
65 | } else {
66 | setError('Failed to reset password');
67 | }
68 | } finally {
69 | setLoading(false);
70 | }
71 | };
72 |
73 | if (success) {
74 | return (
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | Password reset successful
83 |
84 |
85 |
86 | Your password has been successfully reset.
87 | You will be redirected to the app in a moment.
88 |
89 |
90 |
91 | );
92 | }
93 |
94 | return (
95 |
96 |
97 |
98 |
99 |
100 |
101 | Create new password
102 |
103 |
104 |
105 | {error && (
106 |
107 | {error}
108 |
109 | )}
110 |
111 |
161 |
162 | );
163 | }
--------------------------------------------------------------------------------
/nextjs/src/app/auth/verify-email/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { CheckCircle } from 'lucide-react';
4 | import Link from 'next/link';
5 | import {useState} from "react";
6 | import {createSPASassClient} from "@/lib/supabase/client";
7 |
8 | export default function VerifyEmailPage() {
9 | const [email, setEmail] = useState('');
10 | const [loading, setLoading] = useState(false);
11 | const [error, setError] = useState('');
12 | const [success, setSuccess] = useState(false);
13 |
14 | const resendVerificationEmail = async () => {
15 | if (!email) {
16 | setError('Please enter your email address');
17 | return;
18 | }
19 |
20 | try {
21 | setLoading(true);
22 | setError('');
23 | const supabase = await createSPASassClient();
24 | const {error} = await supabase.resendVerificationEmail(email);
25 | if(error) {
26 | setError(error.message);
27 | return;
28 | }
29 | setSuccess(true);
30 | } catch (err: Error | unknown) {
31 | if (err instanceof Error) {
32 | setError(err.message);
33 | } else {
34 | setError('An unknown error occurred');
35 | }
36 | } finally {
37 | setLoading(false);
38 | }
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Check your email
50 |
51 |
52 |
53 | We've sent you an email with a verification link.
54 | Please check your inbox and click the link to verify your account.
55 |
56 |
57 |
58 |
59 | Didn't receive the email? Check your spam folder or enter your email to resend:
60 |
61 |
62 | {error && (
63 |
64 | {error}
65 |
66 | )}
67 |
68 | {success && (
69 |
70 | Verification email has been resent successfully.
71 |
72 | )}
73 |
74 |
75 | setEmail(e.target.value)}
79 | placeholder="Enter your email address"
80 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500 text-sm"
81 | />
82 |
83 |
84 |
89 | {loading ? 'Sending...' : 'Click here to resend'}
90 |
91 |
92 |
93 |
94 |
98 | Return to login
99 |
100 |
101 |
102 |
103 | );
104 | }
--------------------------------------------------------------------------------
/nextjs/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Razikus/supabase-nextjs-template/24068f27f3ed50f391605191f25de1837f1be7e6/nextjs/src/app/favicon.ico
--------------------------------------------------------------------------------
/nextjs/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | /* Color scheme variables */
7 | --color-background: #ffffff;
8 | --color-foreground: #0f172a;
9 |
10 | /* Primary colors */
11 | --color-primary-50: #f0f9ff;
12 | --color-primary-100: #e0f2fe;
13 | --color-primary-200: #bae6fd;
14 | --color-primary-300: #7dd3fc;
15 | --color-primary-400: #38bdf8;
16 | --color-primary-500: #0ea5e9;
17 | --color-primary-600: #0284c7;
18 | --color-primary-700: #0369a1;
19 | --color-primary-800: #075985;
20 | --color-primary-900: #0c4a6e;
21 |
22 | /* Secondary colors */
23 | --color-secondary-50: #f8fafc;
24 | --color-secondary-100: #f1f5f9;
25 | --color-secondary-200: #e2e8f0;
26 | --color-secondary-300: #cbd5e1;
27 | --color-secondary-400: #94a3b8;
28 | --color-secondary-500: #64748b;
29 | --color-secondary-600: #475569;
30 | --color-secondary-700: #334155;
31 | --color-secondary-800: #1e293b;
32 | --color-secondary-900: #0f172a;
33 |
34 | /* Accent colors */
35 | --color-accent-50: #fdf4ff;
36 | --color-accent-100: #fae8ff;
37 | --color-accent-200: #f5d0fe;
38 | --color-accent-300: #f0abfc;
39 | --color-accent-400: #e879f9;
40 | --color-accent-500: #d946ef;
41 | --color-accent-600: #c026d3;
42 | --color-accent-700: #a21caf;
43 | --color-accent-800: #86198f;
44 | --color-accent-900: #701a75;
45 | }
46 |
47 | .theme-blue {
48 | --color-primary-50: #f0f9ff;
49 | --color-primary-100: #e0f2fe;
50 | --color-primary-200: #bae6fd;
51 | --color-primary-300: #7dd3fc;
52 | --color-primary-400: #38bdf8;
53 | --color-primary-500: #0ea5e9;
54 | --color-primary-600: #0284c7;
55 | --color-primary-700: #0369a1;
56 | --color-primary-800: #075985;
57 | --color-primary-900: #0c4a6e;
58 |
59 | --color-secondary-50: #f8fafc;
60 | --color-secondary-100: #f1f5f9;
61 | --color-secondary-200: #e2e8f0;
62 | --color-secondary-300: #cbd5e1;
63 | --color-secondary-400: #94a3b8;
64 | --color-secondary-500: #64748b;
65 | --color-secondary-600: #475569;
66 | --color-secondary-700: #334155;
67 | --color-secondary-800: #1e293b;
68 | --color-secondary-900: #0f172a;
69 |
70 | --color-accent-50: #eef2ff;
71 | --color-accent-100: #e0e7ff;
72 | --color-accent-200: #c7d2fe;
73 | --color-accent-300: #a5b4fc;
74 | --color-accent-400: #818cf8;
75 | --color-accent-500: #6366f1;
76 | --color-accent-600: #4f46e5;
77 | --color-accent-700: #4338ca;
78 | --color-accent-800: #3730a3;
79 | --color-accent-900: #312e81;
80 | }
81 |
82 | .theme-sass {
83 | --color-primary-50: #fdf2f8;
84 | --color-primary-100: #fce7f3;
85 | --color-primary-200: #fbcfe8;
86 | --color-primary-300: #f9a8d4;
87 | --color-primary-400: #f472b6;
88 | --color-primary-500: #cd5c8e;
89 | --color-primary-600: #bf4b8a;
90 | --color-primary-700: #9d3c72;
91 | --color-primary-800: #802d59;
92 | --color-primary-900: #621b3f;
93 |
94 | --color-secondary-50: #f8fafc;
95 | --color-secondary-100: #f1f5f9;
96 | --color-secondary-200: #e2e8f0;
97 | --color-secondary-300: #cbd5e1;
98 | --color-secondary-400: #94a3b8;
99 | --color-secondary-500: #64748b;
100 | --color-secondary-600: #475569;
101 | --color-secondary-700: #334155;
102 | --color-secondary-800: #1e293b;
103 | --color-secondary-900: #0f172a;
104 |
105 | --color-accent-50: #f5f3ff;
106 | --color-accent-100: #ede9fe;
107 | --color-accent-200: #ddd6fe;
108 | --color-accent-300: #c4b5fd;
109 | --color-accent-400: #a78bfa;
110 | --color-accent-500: #8b5cf6;
111 | --color-accent-600: #7c3aed;
112 | --color-accent-700: #6d28d9;
113 | --color-accent-800: #5b21b6;
114 | --color-accent-900: #4c1d95;
115 | }
116 |
117 | .theme-sass2 {
118 | /* Sage/Forest Green Primary */
119 | --color-primary-50: #f4f7f4;
120 | --color-primary-100: #e6ede6;
121 | --color-primary-200: #cfdecf;
122 | --color-primary-300: #a8c2a8;
123 | --color-primary-400: #7fa17f;
124 | --color-primary-500: #5c8a5c;
125 | --color-primary-600: #4a704a;
126 | --color-primary-700: #3d5c3d;
127 | --color-primary-800: #314831;
128 | --color-primary-900: #253825;
129 |
130 | /* Warm Gray Secondary */
131 | --color-secondary-50: #fafaf9;
132 | --color-secondary-100: #f5f5f4;
133 | --color-secondary-200: #e7e5e4;
134 | --color-secondary-300: #d6d3d1;
135 | --color-secondary-400: #a8a29e;
136 | --color-secondary-500: #78716c;
137 | --color-secondary-600: #57534e;
138 | --color-secondary-700: #44403c;
139 | --color-secondary-800: #292524;
140 | --color-secondary-900: #1c1917;
141 |
142 | /* Terracotta Accent */
143 | --color-accent-50: #fdf4f2;
144 | --color-accent-100: #fde8e4;
145 | --color-accent-200: #fad3cc;
146 | --color-accent-300: #f5b3a5;
147 | --color-accent-400: #e98970;
148 | --color-accent-500: #dc6547;
149 | --color-accent-600: #c54a2d;
150 | --color-accent-700: #a33b25;
151 | --color-accent-800: #863225;
152 | --color-accent-900: #6f2b22;
153 | }
154 |
155 | .theme-sass3 {
156 | /* Primary colors - Ocean Blues */
157 | --color-primary-50: #f0f9ff;
158 | --color-primary-100: #e0f7ff;
159 | --color-primary-200: #b9ecff;
160 | --color-primary-300: #7dd5f5;
161 | --color-primary-400: #38b7e8;
162 | --color-primary-500: #0c98d0;
163 | --color-primary-600: #0887c2;
164 | --color-primary-700: #0670a3;
165 | --color-primary-800: #065b86;
166 | --color-primary-900: #074563;
167 |
168 | /* Secondary colors - Sandy Neutrals */
169 | --color-secondary-50: #fdfcfb;
170 | --color-secondary-100: #f8f6f4;
171 | --color-secondary-200: #f0ece7;
172 | --color-secondary-300: #e2dcd4;
173 | --color-secondary-400: #cbc3b9;
174 | --color-secondary-500: #afa497;
175 | --color-secondary-600: #8c8278;
176 | --color-secondary-700: #6e665e;
177 | --color-secondary-800: #504b45;
178 | --color-secondary-900: #353230;
179 |
180 | /* Accent colors - Coral Sunset */
181 | --color-accent-50: #fff5f2;
182 | --color-accent-100: #ffe6e0;
183 | --color-accent-200: #ffc9bc;
184 | --color-accent-300: #ffa592;
185 | --color-accent-400: #ff7a61;
186 | --color-accent-500: #ff5341;
187 | --color-accent-600: #eb3a28;
188 | --color-accent-700: #d12a1a;
189 | --color-accent-800: #b22417;
190 | --color-accent-900: #8f1e15;
191 | }
192 |
193 | .theme-purple {
194 | --color-primary-50: #faf5ff;
195 | --color-primary-100: #f3e8ff;
196 | --color-primary-200: #e9d5ff;
197 | --color-primary-300: #d8b4fe;
198 | --color-primary-400: #c084fc;
199 | --color-primary-500: #a855f7;
200 | --color-primary-600: #9333ea;
201 | --color-primary-700: #7e22ce;
202 | --color-primary-800: #6b21a8;
203 | --color-primary-900: #581c87;
204 |
205 | --color-secondary-50: #fafafa;
206 | --color-secondary-100: #f4f4f5;
207 | --color-secondary-200: #e4e4e7;
208 | --color-secondary-300: #d4d4d8;
209 | --color-secondary-400: #a1a1aa;
210 | --color-secondary-500: #71717a;
211 | --color-secondary-600: #52525b;
212 | --color-secondary-700: #3f3f46;
213 | --color-secondary-800: #27272a;
214 | --color-secondary-900: #18181b;
215 |
216 | --color-accent-50: #fdf2f8;
217 | --color-accent-100: #fce7f3;
218 | --color-accent-200: #fbcfe8;
219 | --color-accent-300: #f9a8d4;
220 | --color-accent-400: #f472b6;
221 | --color-accent-500: #ec4899;
222 | --color-accent-600: #db2777;
223 | --color-accent-700: #be185d;
224 | --color-accent-800: #9d174d;
225 | --color-accent-900: #831843;
226 | }
227 |
228 | .theme-green {
229 | --color-primary-50: #f0fdf4;
230 | --color-primary-100: #dcfce7;
231 | --color-primary-200: #bbf7d0;
232 | --color-primary-300: #86efac;
233 | --color-primary-400: #4ade80;
234 | --color-primary-500: #22c55e;
235 | --color-primary-600: #16a34a;
236 | --color-primary-700: #15803d;
237 | --color-primary-800: #166534;
238 | --color-primary-900: #14532d;
239 |
240 | --color-secondary-50: #fafaf9;
241 | --color-secondary-100: #f5f5f4;
242 | --color-secondary-200: #e7e5e4;
243 | --color-secondary-300: #d6d3d1;
244 | --color-secondary-400: #a8a29e;
245 | --color-secondary-500: #78716c;
246 | --color-secondary-600: #57534e;
247 | --color-secondary-700: #44403c;
248 | --color-secondary-800: #292524;
249 | --color-secondary-900: #1c1917;
250 |
251 | --color-accent-50: #fffbeb;
252 | --color-accent-100: #fef3c7;
253 | --color-accent-200: #fde68a;
254 | --color-accent-300: #fcd34d;
255 | --color-accent-400: #fbbf24;
256 | --color-accent-500: #f59e0b;
257 | --color-accent-600: #d97706;
258 | --color-accent-700: #b45309;
259 | --color-accent-800: #92400e;
260 | --color-accent-900: #78350f;
261 | }
262 |
263 |
264 | @layer components {
265 | .btn-primary {
266 | @apply bg-primary-600 text-white hover:bg-primary-700 transition-colors;
267 | }
268 |
269 | .btn-secondary {
270 | @apply bg-secondary-200 text-secondary-800 hover:bg-secondary-300 transition-colors;
271 | }
272 |
273 | .input-primary {
274 | @apply border-secondary-300 focus:border-primary-500 focus:ring-primary-500;
275 | }
276 | }
277 |
278 |
279 | @layer base {
280 | :root {
281 | --background: 0 0% 100%;
282 | --foreground: 0 0% 3.9%;
283 | --card: 0 0% 100%;
284 | --card-foreground: 0 0% 3.9%;
285 | --popover: 0 0% 100%;
286 | --popover-foreground: 0 0% 3.9%;
287 | --primary: 0 0% 9%;
288 | --primary-foreground: 0 0% 98%;
289 | --secondary: 0 0% 96.1%;
290 | --secondary-foreground: 0 0% 9%;
291 | --muted: 0 0% 96.1%;
292 | --muted-foreground: 0 0% 45.1%;
293 | --accent: 0 0% 96.1%;
294 | --accent-foreground: 0 0% 9%;
295 | --destructive: 0 84.2% 60.2%;
296 | --destructive-foreground: 0 0% 98%;
297 | --border: 0 0% 89.8%;
298 | --input: 0 0% 89.8%;
299 | --ring: 0 0% 3.9%;
300 | --chart-1: 12 76% 61%;
301 | --chart-2: 173 58% 39%;
302 | --chart-3: 197 37% 24%;
303 | --chart-4: 43 74% 66%;
304 | --chart-5: 27 87% 67%;
305 | --radius: 0.5rem;
306 | }
307 | .dark {
308 | --background: 0 0% 3.9%;
309 | --foreground: 0 0% 98%;
310 | --card: 0 0% 3.9%;
311 | --card-foreground: 0 0% 98%;
312 | --popover: 0 0% 3.9%;
313 | --popover-foreground: 0 0% 98%;
314 | --primary: 0 0% 98%;
315 | --primary-foreground: 0 0% 9%;
316 | --secondary: 0 0% 14.9%;
317 | --secondary-foreground: 0 0% 98%;
318 | --muted: 0 0% 14.9%;
319 | --muted-foreground: 0 0% 63.9%;
320 | --accent: 0 0% 14.9%;
321 | --accent-foreground: 0 0% 98%;
322 | --destructive: 0 62.8% 30.6%;
323 | --destructive-foreground: 0 0% 98%;
324 | --border: 0 0% 14.9%;
325 | --input: 0 0% 14.9%;
326 | --ring: 0 0% 83.1%;
327 | --chart-1: 220 70% 50%;
328 | --chart-2: 160 60% 45%;
329 | --chart-3: 30 80% 55%;
330 | --chart-4: 280 65% 60%;
331 | --chart-5: 340 75% 55%;
332 | }
333 | }
334 |
335 |
336 | @layer base {
337 | * {
338 | @apply border-border;
339 | }
340 | body {
341 | @apply bg-background text-foreground;
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/nextjs/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import "./globals.css";
3 | import { Analytics } from '@vercel/analytics/next';
4 | import CookieConsent from "@/components/Cookies";
5 | import { GoogleAnalytics } from '@next/third-parties/google'
6 |
7 |
8 | export const metadata: Metadata = {
9 | title: process.env.NEXT_PUBLIC_PRODUCTNAME,
10 | description: "The best way to build your SaaS product.",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | let theme = process.env.NEXT_PUBLIC_THEME
19 | if(!theme) {
20 | theme = "theme-sass3"
21 | }
22 | const gaID = process.env.NEXT_PUBLIC_GOOGLE_TAG;
23 | return (
24 |
25 |
26 | {children}
27 |
28 |
29 | { gaID && (
30 |
31 | )}
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/nextjs/src/app/legal/[document]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import LegalDocument from '@/components/LegalDocument';
5 | import { notFound } from 'next/navigation';
6 |
7 | const legalDocuments = {
8 | 'privacy': {
9 | title: 'Privacy Notice',
10 | path: '/terms/privacy-notice.md'
11 | },
12 | 'terms': {
13 | title: 'Terms of Service',
14 | path: '/terms/terms-of-service.md'
15 | },
16 | 'refund': {
17 | title: 'Refund Policy',
18 | path: '/terms/refund-policy.md'
19 | }
20 | } as const;
21 |
22 | type LegalDocument = keyof typeof legalDocuments;
23 |
24 | interface LegalPageProps {
25 | document: LegalDocument;
26 | lng: string;
27 | }
28 |
29 | interface LegalPageParams {
30 | params: Promise
31 | }
32 |
33 | export default function LegalPage({ params }: LegalPageParams) {
34 | const {document} = React.use(params);
35 |
36 | if (!legalDocuments[document]) {
37 | notFound();
38 | }
39 |
40 | const { title, path } = legalDocuments[document];
41 |
42 | return (
43 |
44 |
48 |
49 | );
50 | }
--------------------------------------------------------------------------------
/nextjs/src/app/legal/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from 'react';
3 | import Link from 'next/link';
4 | import { useRouter } from 'next/navigation';
5 | import { ArrowLeft, FileText, ShieldAlert, RefreshCw } from 'lucide-react';
6 |
7 | const legalDocuments = [
8 | {
9 | id: 'privacy',
10 | title: 'Privacy Policy',
11 | icon: ShieldAlert,
12 | description: 'How we handle and protect your data'
13 | },
14 | {
15 | id: 'terms',
16 | title: 'Terms of Service',
17 | icon: FileText,
18 | description: 'Rules and guidelines for using our service'
19 | },
20 | {
21 | id: 'refund',
22 | title: 'Refund Policy',
23 | icon: RefreshCw,
24 | description: 'Our policy on refunds and cancellations'
25 | }
26 | ];
27 |
28 | export default function LegalLayout({ children } : { children: React.ReactNode }) {
29 | const router = useRouter();
30 |
31 | return (
32 |
33 |
34 |
35 |
router.back()}
37 | className="inline-flex items-center text-sm text-gray-600 hover:text-gray-900"
38 | >
39 |
40 | Back
41 |
42 |
43 |
44 |
45 | {/* Sidebar Navigation */}
46 |
47 |
48 |
49 |
Legal Documents
50 |
Important information about our services
51 |
52 |
53 | {legalDocuments.map((doc) => (
54 |
59 |
60 |
61 |
62 |
{doc.title}
63 |
{doc.description}
64 |
65 |
66 |
67 | ))}
68 |
69 |
70 |
71 |
72 | {/* Main Content */}
73 |
74 | {children}
75 |
76 |
77 |
78 |
79 | );
80 | }
--------------------------------------------------------------------------------
/nextjs/src/app/legal/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | 'use client';
3 |
4 | import React from 'react';
5 |
6 |
7 | export default function LegalPage() {
8 |
9 |
10 | return (
11 |
12 | Select document on the left.
13 |
14 | );
15 | }
--------------------------------------------------------------------------------
/nextjs/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { ArrowRight, Globe, Shield, Users, Key, Database, Clock } from 'lucide-react';
4 | import AuthAwareButtons from '@/components/AuthAwareButtons';
5 | import HomePricing from "@/components/HomePricing";
6 |
7 | export default function Home() {
8 | const productName = process.env.NEXT_PUBLIC_PRODUCTNAME;
9 |
10 | const features = [
11 | {
12 | icon: Shield,
13 | title: 'Robust Authentication',
14 | description: 'Secure login with email/password, Multi-Factor Authentication, and SSO providers',
15 | color: 'text-green-600'
16 | },
17 | {
18 | icon: Database,
19 | title: 'File Management',
20 | description: 'Built-in file storage with secure sharing, downloads, and granular permissions',
21 | color: 'text-orange-600'
22 | },
23 | {
24 | icon: Users,
25 | title: 'User Settings',
26 | description: 'Complete user management with password updates, MFA setup, and profile controls',
27 | color: 'text-red-600'
28 | },
29 | {
30 | icon: Clock,
31 | title: 'Task Management',
32 | description: 'Built-in todo system with real-time updates and priority management',
33 | color: 'text-teal-600'
34 | },
35 | {
36 | icon: Globe,
37 | title: 'Legal Documents',
38 | description: 'Pre-configured privacy policy, terms of service, and refund policy pages',
39 | color: 'text-purple-600'
40 | },
41 | {
42 | icon: Key,
43 | title: 'Cookie Consent',
44 | description: 'GDPR-compliant cookie consent system with customizable preferences',
45 | color: 'text-blue-600'
46 | }
47 | ];
48 |
49 | const stats = [
50 | { label: 'Active Users', value: '10K+' },
51 | { label: 'Organizations', value: '2K+' },
52 | { label: 'Countries', value: '50+' },
53 | { label: 'Uptime', value: '99.9%' }
54 | ];
55 |
56 | return (
57 |
58 |
59 |
60 |
61 |
62 |
63 | {productName}
64 |
65 |
66 |
67 |
68 | Features
69 |
70 |
71 |
72 | Pricing
73 |
74 |
80 | Documentation
81 |
82 |
83 |
89 | Grab This Template
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | Bootstrap Your SaaS
103 | In 5 minutes
104 |
105 |
106 | Launch your SaaS product in days, not months. Complete with authentication and enterprise-grade security built right in.
107 |
108 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {stats.map((stat, index) => (
120 |
121 |
{stat.value}
122 |
{stat.label}
123 |
124 | ))}
125 |
126 |
127 |
128 |
129 | {/* Features Section */}
130 |
131 |
132 |
133 |
Everything You Need
134 |
135 | Built with modern technologies for reliability and speed
136 |
137 |
138 |
139 | {features.map((feature, index) => (
140 |
144 |
145 |
{feature.title}
146 |
{feature.description}
147 |
148 | ))}
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | Ready to Transform Your Idea into Reality?
159 |
160 |
161 | Join thousands of developers building their SaaS with {productName}
162 |
163 |
167 | Get Started Now
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
Product
178 |
179 |
180 |
181 | Features
182 |
183 |
184 |
185 |
186 | Pricing
187 |
188 |
189 |
190 |
191 |
192 |
Resources
193 |
194 |
195 |
196 | Documentation
197 |
198 |
199 |
200 |
201 |
202 |
Legal
203 |
204 |
205 |
206 | Privacy
207 |
208 |
209 |
210 |
211 | Terms
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | © {new Date().getFullYear()} {productName}. All rights reserved.
220 |
221 |
222 |
223 |
224 |
225 | );
226 | }
--------------------------------------------------------------------------------
/nextjs/src/components/AppLayout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from 'react';
3 | import Link from 'next/link';
4 | import {usePathname, useRouter} from 'next/navigation';
5 | import {
6 | Home,
7 | User,
8 | Menu,
9 | X,
10 | ChevronDown,
11 | LogOut,
12 | Key, Files, LucideListTodo,
13 | } from 'lucide-react';
14 | import { useGlobal } from "@/lib/context/GlobalContext";
15 | import { createSPASassClient } from "@/lib/supabase/client";
16 |
17 | export default function AppLayout({ children }: { children: React.ReactNode }) {
18 | const [isSidebarOpen, setSidebarOpen] = useState(false);
19 | const [isUserDropdownOpen, setUserDropdownOpen] = useState(false);
20 | const pathname = usePathname();
21 | const router = useRouter();
22 |
23 |
24 | const { user } = useGlobal();
25 |
26 | const handleLogout = async () => {
27 | try {
28 | const client = await createSPASassClient();
29 | await client.logout();
30 | } catch (error) {
31 | console.error('Error logging out:', error);
32 | }
33 | };
34 | const handleChangePassword = async () => {
35 | router.push('/app/user-settings')
36 | };
37 |
38 | const getInitials = (email: string) => {
39 | const parts = email.split('@')[0].split(/[._-]/);
40 | return parts.length > 1
41 | ? (parts[0][0] + parts[1][0]).toUpperCase()
42 | : parts[0].slice(0, 2).toUpperCase();
43 | };
44 |
45 | const productName = process.env.NEXT_PUBLIC_PRODUCTNAME;
46 |
47 | const navigation = [
48 | { name: 'Homepage', href: '/app', icon: Home },
49 | { name: 'Example Storage', href: '/app/storage', icon: Files },
50 | { name: 'Example Table', href: '/app/table', icon: LucideListTodo },
51 | { name: 'User Settings', href: '/app/user-settings', icon: User },
52 | ];
53 |
54 | const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
55 |
56 | return (
57 |
58 | {isSidebarOpen && (
59 |
63 | )}
64 |
65 | {/* Sidebar */}
66 |
68 |
69 |
70 | {productName}
71 |
75 |
76 |
77 |
78 |
79 | {/* Navigation */}
80 |
81 | {navigation.map((item) => {
82 | const isActive = pathname === item.href;
83 | return (
84 |
93 |
98 | {item.name}
99 |
100 | );
101 | })}
102 |
103 |
104 |
105 |
106 |
107 |
108 |
112 |
113 |
114 |
115 |
116 |
setUserDropdownOpen(!isUserDropdownOpen)}
118 | className="flex items-center space-x-2 text-sm text-gray-700 hover:text-gray-900"
119 | >
120 |
121 |
122 | {user ? getInitials(user.email) : '??'}
123 |
124 |
125 | {user?.email || 'Loading...'}
126 |
127 |
128 |
129 | {isUserDropdownOpen && (
130 |
131 |
132 |
Signed in as
133 |
134 | {user?.email}
135 |
136 |
137 |
138 | {
140 | setUserDropdownOpen(false);
141 | handleChangePassword()
142 | }}
143 | className="w-full flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
144 | >
145 |
146 | Change Password
147 |
148 | {
150 | handleLogout();
151 | setUserDropdownOpen(false);
152 | }}
153 | className="w-full flex items-center px-4 py-2 text-sm text-red-600 hover:bg-red-50"
154 | >
155 |
156 | Sign Out
157 |
158 |
159 |
160 | )}
161 |
162 |
163 |
164 |
165 | {children}
166 |
167 |
168 |
169 | );
170 | }
--------------------------------------------------------------------------------
/nextjs/src/components/AuthAwareButtons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState, useEffect } from 'react';
3 | import { createSPASassClient } from '@/lib/supabase/client';
4 | import { ArrowRight, ChevronRight } from 'lucide-react';
5 | import Link from "next/link";
6 |
7 | export default function AuthAwareButtons({ variant = 'primary' }) {
8 | const [isAuthenticated, setIsAuthenticated] = useState(false);
9 | const [loading, setLoading] = useState(true);
10 |
11 | useEffect(() => {
12 | const checkAuth = async () => {
13 | try {
14 | const supabase = await createSPASassClient();
15 | const { data: { user } } = await supabase.getSupabaseClient().auth.getUser();
16 | setIsAuthenticated(!!user);
17 | } catch (error) {
18 | console.error('Error checking auth status:', error);
19 | } finally {
20 | setLoading(false);
21 | }
22 | };
23 |
24 | checkAuth();
25 | }, []);
26 |
27 | if (loading) {
28 | return null;
29 | }
30 |
31 | // Navigation buttons for the header
32 | if (variant === 'nav') {
33 | return isAuthenticated ? (
34 |
38 | Go to Dashboard
39 |
40 | ) : (
41 | <>
42 |
43 | Login
44 |
45 |
49 | Get Started
50 |
51 | >
52 | );
53 | }
54 |
55 | // Primary buttons for the hero section
56 | return isAuthenticated ? (
57 |
61 | Go to Dashboard
62 |
63 |
64 | ) : (
65 | <>
66 |
70 | Start Building Free
71 |
72 |
73 |
77 | Learn More
78 |
79 |
80 | >
81 | );
82 | }
--------------------------------------------------------------------------------
/nextjs/src/components/Confetti.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect, useRef } from 'react';
3 |
4 | interface ConfettiProps {
5 | active: boolean;
6 | }
7 |
8 | interface Particle {
9 | x: number;
10 | y: number;
11 | size: number;
12 | color: string;
13 | speedX: number;
14 | speedY: number;
15 | gravity: number;
16 | rotation: number;
17 | rotationSpeed: number;
18 | }
19 |
20 | const Confetti: React.FC = ({ active }) => {
21 | const canvasRef = useRef(null);
22 | const animationFrameId = useRef(null);
23 | const particles = useRef([]);
24 |
25 | useEffect(() => {
26 | if (!active || !canvasRef.current) return;
27 |
28 | const canvas = canvasRef.current;
29 | const ctx = canvas.getContext('2d');
30 | if (!ctx) return;
31 |
32 | const colors = ['#ffd700', '#ff0000', '#00ff00', '#0000ff', '#ff00ff'];
33 |
34 | particles.current = Array.from({ length: 50 }, (): Particle => ({
35 | x: canvas.width / 2,
36 | y: canvas.height / 2,
37 | size: Math.random() * 8 + 4,
38 | color: colors[Math.floor(Math.random() * colors.length)],
39 | speedX: (Math.random() - 0.5) * 12,
40 | speedY: -Math.random() * 15 - 5,
41 | gravity: 0.7,
42 | rotation: Math.random() * 360,
43 | rotationSpeed: (Math.random() - 0.5) * 8
44 | }));
45 |
46 | let frame = 0;
47 | const maxFrames = 120;
48 |
49 | const animate = () => {
50 | ctx.clearRect(0, 0, canvas.width, canvas.height);
51 |
52 | particles.current.forEach((particle) => {
53 | ctx.save();
54 | ctx.translate(particle.x, particle.y);
55 | ctx.rotate((particle.rotation * Math.PI) / 180);
56 |
57 | ctx.fillStyle = particle.color;
58 | ctx.fillRect(-particle.size / 2, -particle.size / 2, particle.size, particle.size);
59 |
60 | ctx.restore();
61 |
62 | particle.x += particle.speedX;
63 | particle.y += particle.speedY;
64 | particle.speedY += particle.gravity;
65 | particle.rotation += particle.rotationSpeed;
66 | });
67 |
68 | frame++;
69 |
70 | if (frame < maxFrames) {
71 | animationFrameId.current = requestAnimationFrame(animate);
72 | }
73 | };
74 |
75 | animate();
76 |
77 | return () => {
78 | if (animationFrameId.current) {
79 | cancelAnimationFrame(animationFrameId.current);
80 | }
81 | };
82 | }, [active]);
83 |
84 | return (
85 |
91 | );
92 | };
93 |
94 | export default Confetti;
--------------------------------------------------------------------------------
/nextjs/src/components/Cookies.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect, useState } from 'react';
3 | import { Button } from '@/components/ui/button';
4 | import { Shield, X } from 'lucide-react';
5 | import { setCookie, getCookie } from 'cookies-next/client';
6 | import Link from 'next/link';
7 |
8 | const COOKIE_CONSENT_KEY = 'cookie-accept';
9 | const COOKIE_EXPIRY_DAYS = 365;
10 |
11 | const CookieConsent = () => {
12 | const [isVisible, setIsVisible] = useState(false);
13 |
14 | useEffect(() => {
15 | const consent = getCookie(COOKIE_CONSENT_KEY);
16 | if (!consent) {
17 | const timer = setTimeout(() => {
18 | setIsVisible(true);
19 | }, 1000);
20 | return () => clearTimeout(timer);
21 | }
22 | }, []);
23 |
24 | const handleAccept = () => {
25 | setCookie(COOKIE_CONSENT_KEY, 'accepted', {
26 | expires: new Date(Date.now() + COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000),
27 | sameSite: 'strict',
28 | secure: process.env.NODE_ENV === 'production',
29 | path: '/'
30 | });
31 | setIsVisible(false);
32 | };
33 |
34 | const handleDecline = () => {
35 | setCookie(COOKIE_CONSENT_KEY, 'declined', {
36 | expires: new Date(Date.now() + COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000),
37 | sameSite: 'strict',
38 | secure: process.env.NODE_ENV === 'production',
39 | path: '/'
40 | });
41 | setIsVisible(false);
42 | };
43 |
44 | if (!isVisible) return null;
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic.
55 | By clicking "Accept", you consent to our use of cookies.
56 |
57 |
58 | Read our{' '}
59 |
60 | Privacy Policy
61 | {' '}
62 | and{' '}
63 |
64 | Terms of Service
65 | {' '}
66 | for more information.
67 |
68 |
69 |
70 |
71 |
77 | Decline
78 |
79 |
84 | Accept
85 |
86 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | export default CookieConsent;
--------------------------------------------------------------------------------
/nextjs/src/components/HomePricing.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from 'react';
3 | import Link from 'next/link';
4 | import { Check } from 'lucide-react';
5 | import PricingService from "@/lib/pricing";
6 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
7 |
8 | const HomePricing = () => {
9 | const tiers = PricingService.getAllTiers();
10 | const commonFeatures = PricingService.getCommonFeatures();
11 |
12 | return (
13 |
14 |
15 |
16 |
Simple, Transparent Pricing
17 |
Choose the plan that's right for your business (IT'S PLACEHOLDER NO PRICING FOR THIS TEMPLATE)
18 |
19 |
20 |
21 | {tiers.map((tier) => (
22 |
28 | {tier.popular && (
29 |
30 | Most Popular
31 |
32 | )}
33 |
34 |
35 | {tier.name}
36 | {tier.description}
37 |
38 |
39 |
40 |
41 | {PricingService.formatPrice(tier.price)}
42 | /month
43 |
44 |
45 |
46 | {tier.features.map((feature) => (
47 |
48 |
49 | {feature}
50 |
51 | ))}
52 |
53 |
54 |
62 | Get Started
63 |
64 |
65 |
66 | ))}
67 |
68 |
69 |
70 |
71 | All plans include: {commonFeatures.join(', ')}
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default HomePricing;
80 |
--------------------------------------------------------------------------------
/nextjs/src/components/LegalDocument.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from 'react';
4 | import ReactMarkdown from 'react-markdown';
5 | import { Card, CardHeader, CardContent } from '@/components/ui/card';
6 | import { Loader2 } from 'lucide-react';
7 |
8 | interface LegalDocumentProps {
9 | filePath: string;
10 | title: string;
11 | }
12 |
13 | const LegalDocument: React.FC = ({ filePath, title }) => {
14 | const [content, setContent] = useState('');
15 | const [loading, setLoading] = useState(true);
16 | const [error, setError] = useState(null);
17 |
18 | useEffect(() => {
19 | setLoading(true);
20 | setError(null);
21 |
22 | fetch(filePath)
23 | .then(response => {
24 | if (!response.ok) {
25 | throw new Error('Failed to load document');
26 | }
27 | return response.text();
28 | })
29 | .then(text => {
30 | setContent(text);
31 | setLoading(false);
32 | })
33 | .catch(error => {
34 | console.error('Error loading markdown:', error);
35 | setError('Failed to load document. Please try again later.');
36 | setLoading(false);
37 | });
38 | }, [filePath]);
39 |
40 | return (
41 |
42 |
43 | {title}
44 |
45 |
46 | {loading ? (
47 |
48 |
49 |
50 | ) : error ? (
51 |
52 | {error}
53 |
54 | ) : (
55 | {children} ,
58 | h2: ({ children }) => {children} ,
59 | h3: ({ children }) => {children} ,
60 | ul: ({ children }) => ,
61 | li: ({ children }) => {children} ,
62 | p: ({ children }) => {children}
,
63 | }}
64 | >
65 | {content}
66 |
67 | )}
68 |
69 |
70 | );
71 | };
72 |
73 | export default LegalDocument;
--------------------------------------------------------------------------------
/nextjs/src/components/LegalDocuments.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from "react";
3 | import {FileText, RefreshCw, Shield} from "lucide-react";
4 | import Link from "next/link";
5 |
6 | type LegalDocumentsParams = {
7 | minimalist: boolean;
8 | }
9 |
10 | export default function LegalDocuments({ minimalist }: LegalDocumentsParams) {
11 | if (minimalist) {
12 | return (
13 | <>
14 |
15 |
16 | Legal Documents
17 |
18 |
19 |
23 | Privacy Policy
24 |
25 |
29 | Terms of Service
30 |
31 |
35 | Refund Policy
36 |
37 |
38 | >
39 | )
40 | }
41 | return (
42 | <>
43 |
44 |
45 | Legal Documents
46 |
47 |
48 |
52 |
53 | Privacy Policy
55 |
56 |
60 |
61 | Terms of Service
63 |
64 |
68 |
69 | Refund Policy
71 |
72 |
73 | >
74 | )
75 | }
--------------------------------------------------------------------------------
/nextjs/src/components/MFAVerification.tsx:
--------------------------------------------------------------------------------
1 | // src/components/MFAVerification.tsx
2 | import React, { useState, useEffect } from 'react';
3 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
4 | import { Alert, AlertDescription } from '@/components/ui/alert';
5 | import { createSPASassClient } from '@/lib/supabase/client';
6 | import { CheckCircle, Smartphone } from 'lucide-react';
7 | import { Factor } from '@supabase/auth-js';
8 |
9 | interface MFAVerificationProps {
10 | onVerified: () => void;
11 | }
12 |
13 | export function MFAVerification({ onVerified }: MFAVerificationProps) {
14 | const [verifyCode, setVerifyCode] = useState('');
15 | const [error, setError] = useState('');
16 | const [loading, setLoading] = useState(false);
17 | const [factors, setFactors] = useState([]);
18 | const [selectedFactorId, setSelectedFactorId] = useState('');
19 | const [loadingFactors, setLoadingFactors] = useState(true);
20 |
21 | useEffect(() => {
22 | loadFactors();
23 | }, []);
24 |
25 | const loadFactors = async () => {
26 | try {
27 | const supabase = await createSPASassClient();
28 | const { data, error } = await supabase.getSupabaseClient().auth.mfa.listFactors();
29 |
30 | if (error) throw error;
31 |
32 | const totpFactors = data.totp || [];
33 | setFactors(totpFactors);
34 | console.log('totpFactors:', totpFactors);
35 |
36 | // If there's only one factor, select it automatically
37 | if (totpFactors.length === 1) {
38 | setSelectedFactorId(totpFactors[0].id);
39 | }
40 | } catch (err) {
41 | console.error('Error loading MFA factors:', err);
42 | setError('Failed to load authentication devices');
43 | } finally {
44 | setLoadingFactors(false);
45 | }
46 | };
47 |
48 | const handleVerification = async () => {
49 | if (!selectedFactorId) {
50 | setError('Please select an authentication device');
51 | return;
52 | }
53 |
54 | setError('');
55 | setLoading(true);
56 |
57 | try {
58 | const supabase = await createSPASassClient();
59 | const client = supabase.getSupabaseClient();
60 |
61 | // Create challenge
62 | const { data: challengeData, error: challengeError } = await client.auth.mfa.challenge({
63 | factorId: selectedFactorId
64 | });
65 |
66 | if (challengeError) throw challengeError;
67 |
68 | // Verify the challenge
69 | const { error: verifyError } = await client.auth.mfa.verify({
70 | factorId: selectedFactorId,
71 | challengeId: challengeData.id,
72 | code: verifyCode
73 | });
74 |
75 | if (verifyError) throw verifyError;
76 |
77 | onVerified();
78 | } catch (err) {
79 | console.error('MFA verification error:', err);
80 | setError(err instanceof Error ? err.message : 'Failed to verify MFA code');
81 | } finally {
82 | setLoading(false);
83 | }
84 | };
85 |
86 | if (loadingFactors) {
87 | return (
88 |
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | if (factors.length === 0) {
97 | return (
98 |
99 |
100 |
101 |
102 | No authentication devices found. Please contact support.
103 |
104 |
105 |
106 |
107 | );
108 | }
109 |
110 | return (
111 |
112 |
113 | Two-Factor Authentication Required
114 |
115 | Please enter the verification code from your authenticator app
116 |
117 |
118 |
119 | {error && (
120 |
121 | {error}
122 |
123 | )}
124 |
125 |
126 | {factors.length > 1 && (
127 |
128 |
129 | Select Authentication Device
130 |
131 |
132 | {factors.map((factor) => (
133 |
setSelectedFactorId(factor.id)}
136 | className={`flex items-center space-x-3 p-3 border rounded-lg transition-colors ${
137 | selectedFactorId === factor.id
138 | ? 'border-primary-500 bg-primary-50 text-primary-700'
139 | : 'border-gray-200 hover:border-primary-200 hover:bg-gray-50'
140 | }`}
141 | >
142 |
143 |
144 |
145 | {factor.friendly_name || 'Authenticator Device'}
146 |
147 |
148 | Added on {new Date(factor.created_at).toLocaleDateString()}
149 |
150 |
151 | {selectedFactorId === factor.id && (
152 |
153 | )}
154 |
155 | ))}
156 |
157 |
158 | )}
159 |
160 |
161 |
162 | Verification Code
163 |
164 |
setVerifyCode(e.target.value.trim())}
168 | className="block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500 sm:text-sm"
169 | placeholder="Enter 6-digit code"
170 | maxLength={6}
171 | />
172 |
173 | Enter the 6-digit code from your authenticator app
174 |
175 |
176 |
177 |
182 | {loading ? 'Verifying...' : 'Verify'}
183 |
184 |
185 |
186 |
187 | );
188 | }
--------------------------------------------------------------------------------
/nextjs/src/components/SSOButtons.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { createSPAClient } from '@/lib/supabase/client';
4 | import Link from "next/link";
5 |
6 | type Provider = 'github' | 'google' | 'facebook' | 'apple';
7 |
8 | interface SSOButtonsProps {
9 | onError?: (error: string) => void;
10 | }
11 |
12 | const PROVIDER_CONFIGS = {
13 | github: {
14 | name: 'GitHub',
15 | icon: (
16 |
17 |
18 |
19 | ),
20 | bgColor: 'bg-gray-800 hover:bg-gray-700',
21 | textColor: 'text-white',
22 | borderColor: 'border-transparent'
23 | },
24 | google: {
25 | name: 'Google',
26 | icon: (
27 |
28 |
29 |
30 |
31 |
32 |
33 | ),
34 | bgColor: 'bg-white hover:bg-gray-50',
35 | textColor: 'text-gray-700',
36 | borderColor: 'border-gray-300'
37 | },
38 | facebook: {
39 | name: 'Facebook',
40 | icon: (
41 |
42 |
43 |
44 | ),
45 | bgColor: 'bg-[#1877F2] hover:bg-[#166fe5]',
46 | textColor: 'text-white',
47 | borderColor: 'border-transparent'
48 | },
49 | apple: {
50 | name: 'Apple',
51 | icon: (
52 |
53 |
54 |
55 | ),
56 | bgColor: 'bg-black hover:bg-gray-900',
57 | textColor: 'text-white',
58 | borderColor: 'border-transparent'
59 | }
60 | };
61 |
62 | function getEnabledProviders(): Provider[] {
63 | const providersStr = process.env.NEXT_PUBLIC_SSO_PROVIDERS || '';
64 | return providersStr.split(',').filter((provider): provider is Provider =>
65 | provider.trim().toLowerCase() in PROVIDER_CONFIGS
66 | );
67 | }
68 |
69 | export default function SSOButtons({ onError }: SSOButtonsProps) {
70 | const handleSSOLogin = async (provider: Provider) => {
71 | try {
72 | const supabase = createSPAClient();
73 | const { error } = await supabase.auth.signInWithOAuth({
74 | provider,
75 | options: {
76 | redirectTo: `${window.location.origin}/api/auth/callback`,
77 | },
78 | });
79 |
80 | if (error) throw error;
81 | } catch (err: Error | unknown) {
82 | if (err instanceof Error) {
83 | onError?.(err.message);
84 | } else {
85 | onError?.('An unknown error occurred');
86 | }
87 | }
88 | };
89 |
90 | const enabledProviders = getEnabledProviders();
91 |
92 | if (enabledProviders.length === 0) {
93 | return null;
94 | }
95 |
96 | return (
97 |
98 |
99 |
102 |
103 | Or continue with
104 |
105 |
106 |
107 |
108 | {enabledProviders.map((provider) => {
109 | const config = PROVIDER_CONFIGS[provider];
110 | return (
111 |
handleSSOLogin(provider)}
114 | className={`group relative flex h-11 items-center rounded-md border ${config.borderColor} px-6 transition-colors duration-200 ${config.bgColor} ${config.textColor}`}
115 | >
116 |
117 |
118 | {config.icon}
119 |
120 |
121 |
122 | Continue with {config.name}
123 |
124 |
125 | );
126 | })}
127 |
128 |
129 | By creating an account via selected provider, you agree to our{' '}
130 |
131 | Terms and Conditions
132 |
133 | {' '}and{' '}
134 |
135 | Privacy Policy
136 |
137 |
138 |
139 | );
140 | }
--------------------------------------------------------------------------------
/nextjs/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/nextjs/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/nextjs/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/nextjs/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/nextjs/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/nextjs/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/nextjs/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/nextjs/src/lib/context/GlobalContext.tsx:
--------------------------------------------------------------------------------
1 | // src/lib/context/GlobalContext.tsx
2 | 'use client';
3 |
4 | import React, { createContext, useContext, useState, useEffect } from 'react';
5 | import { createSPASassClient } from '@/lib/supabase/client';
6 |
7 |
8 | type User = {
9 | email: string;
10 | id: string;
11 | registered_at: Date;
12 | };
13 |
14 | interface GlobalContextType {
15 | loading: boolean;
16 | user: User | null; // Add this
17 | }
18 |
19 | const GlobalContext = createContext(undefined);
20 |
21 | export function GlobalProvider({ children }: { children: React.ReactNode }) {
22 | const [loading, setLoading] = useState(true);
23 | const [user, setUser] = useState(null); // Add this
24 |
25 | useEffect(() => {
26 | async function loadData() {
27 | try {
28 | const supabase = await createSPASassClient();
29 | const client = supabase.getSupabaseClient();
30 |
31 | // Get user data
32 | const { data: { user } } = await client.auth.getUser();
33 | if (user) {
34 | setUser({
35 | email: user.email!,
36 | id: user.id,
37 | registered_at: new Date(user.created_at)
38 | });
39 | } else {
40 | throw new Error('User not found');
41 | }
42 |
43 | } catch (error) {
44 | console.error('Error loading data:', error);
45 | } finally {
46 | setLoading(false);
47 | }
48 | }
49 |
50 | loadData();
51 | }, []);
52 |
53 | return (
54 |
55 | {children}
56 |
57 | );
58 | }
59 |
60 | export const useGlobal = () => {
61 | const context = useContext(GlobalContext);
62 | if (context === undefined) {
63 | throw new Error('useGlobal must be used within a GlobalProvider');
64 | }
65 | return context;
66 | };
--------------------------------------------------------------------------------
/nextjs/src/lib/pricing.ts:
--------------------------------------------------------------------------------
1 | export interface PricingTier {
2 | name: string;
3 | price: number;
4 | description: string;
5 | features: string[];
6 | popular?: boolean;
7 | }
8 |
9 | class PricingService {
10 | private static tiers: PricingTier[] = [];
11 |
12 | static initialize() {
13 | const names = process.env.NEXT_PUBLIC_TIERS_NAMES?.split(',') || [];
14 | const prices = process.env.NEXT_PUBLIC_TIERS_PRICES?.split(',').map(Number) || [];
15 | const descriptions = process.env.NEXT_PUBLIC_TIERS_DESCRIPTIONS?.split(',') || [];
16 | const features = process.env.NEXT_PUBLIC_TIERS_FEATURES?.split(',').map(f => f.split('|')) || [];
17 | const popularTier = process.env.NEXT_PUBLIC_POPULAR_TIER;
18 |
19 | this.tiers = names.map((name, index) => ({
20 | name,
21 | price: prices[index],
22 | description: descriptions[index],
23 | features: features[index] || [],
24 | popular: name === popularTier
25 | }));
26 | }
27 |
28 | static getAllTiers(): PricingTier[] {
29 | if (this.tiers.length === 0) {
30 | this.initialize();
31 | }
32 | return this.tiers;
33 | }
34 |
35 | static getCommonFeatures(): string[] {
36 | return process.env.NEXT_PUBLIC_COMMON_FEATURES?.split(',') || [];
37 | }
38 |
39 | static formatPrice(price: number): string {
40 | return `$${price}`;
41 | }
42 |
43 | }
44 |
45 | export default PricingService;
--------------------------------------------------------------------------------
/nextjs/src/lib/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import {createBrowserClient} from '@supabase/ssr'
2 | import {ClientType, SassClient} from "@/lib/supabase/unified";
3 | import {Database} from "@/lib/types";
4 |
5 | export function createSPAClient() {
6 | return createBrowserClient(
7 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
8 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
9 | )
10 | }
11 |
12 | export async function createSPASassClient() {
13 | const client = createSPAClient();
14 | return new SassClient(client, ClientType.SPA);
15 | }
--------------------------------------------------------------------------------
/nextjs/src/lib/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { NextResponse, type NextRequest } from 'next/server'
3 |
4 | export async function updateSession(request: NextRequest) {
5 | let supabaseResponse = NextResponse.next({
6 | request,
7 | })
8 |
9 | const supabase = createServerClient(
10 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return request.cookies.getAll()
16 | },
17 | setAll(cookiesToSet) {
18 | cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
19 | supabaseResponse = NextResponse.next({
20 | request,
21 | })
22 | cookiesToSet.forEach(({ name, value, options }) =>
23 | supabaseResponse.cookies.set(name, value, options)
24 | )
25 | },
26 | },
27 | }
28 | )
29 |
30 | // Do not run code between createServerClient and
31 | // supabase.auth.getUser(). A simple mistake could make it very hard to debug
32 | // issues with users being randomly logged out.
33 |
34 | // IMPORTANT: DO NOT REMOVE auth.getUser()
35 |
36 | const {data: user} = await supabase.auth.getUser()
37 | if (
38 | (!user || !user.user) && request.nextUrl.pathname.startsWith('/app')
39 | ) {
40 | const url = request.nextUrl.clone()
41 | url.pathname = '/auth/login'
42 | return NextResponse.redirect(url)
43 | }
44 |
45 | // IMPORTANT: You *must* return the supabaseResponse object as it is.
46 | // If you're creating a new response object with NextResponse.next() make sure to:
47 | // 1. Pass the request in it, like so:
48 | // const myNewResponse = NextResponse.next({ request })
49 | // 2. Copy over the cookies, like so:
50 | // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
51 | // 3. Change the myNewResponse object to fit your needs, but avoid changing
52 | // the cookies!
53 | // 4. Finally:
54 | // return myNewResponse
55 | // If this is not done, you may be causing the browser and server to go out
56 | // of sync and terminate the user's session prematurely!
57 |
58 | return supabaseResponse
59 | }
--------------------------------------------------------------------------------
/nextjs/src/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import {createServerClient} from '@supabase/ssr'
2 | import {cookies} from 'next/headers'
3 | import {ClientType, SassClient} from "@/lib/supabase/unified";
4 | import {Database} from "@/lib/types";
5 |
6 | export async function createSSRClient() {
7 | const cookieStore = await cookies()
8 |
9 | return createServerClient(
10 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return cookieStore.getAll()
16 | },
17 | setAll(cookiesToSet) {
18 | try {
19 | cookiesToSet.forEach(({ name, value, options }) =>
20 | cookieStore.set(name, value, options)
21 | )
22 | } catch {
23 | // The `setAll` method was called from a Server Component.
24 | // This can be ignored if you have middleware refreshing
25 | // user sessions.
26 | }
27 | },
28 | }
29 | }
30 | )
31 | }
32 |
33 |
34 |
35 | export async function createSSRSassClient() {
36 | const client = await createSSRClient();
37 | return new SassClient(client, ClientType.SERVER);
38 | }
--------------------------------------------------------------------------------
/nextjs/src/lib/supabase/serverAdminClient.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import {Database} from "@/lib/types";
3 |
4 | export async function createServerAdminClient() {
5 |
6 | return createServerClient(
7 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
8 | process.env.PRIVATE_SUPABASE_SERVICE_KEY!,
9 | {
10 | cookies: {
11 | getAll: () => [],
12 | setAll: () => {},
13 | },
14 | auth: {
15 | persistSession: false,
16 | autoRefreshToken: false,
17 | },
18 | db: {
19 | schema: 'public'
20 | },
21 | }
22 | )
23 | }
--------------------------------------------------------------------------------
/nextjs/src/lib/supabase/unified.ts:
--------------------------------------------------------------------------------
1 | import {SupabaseClient} from "@supabase/supabase-js";
2 | import {Database} from "@/lib/types";
3 |
4 | export enum ClientType {
5 | SERVER = 'server',
6 | SPA = 'spa'
7 | }
8 |
9 | export class SassClient {
10 | private client: SupabaseClient;
11 | private clientType: ClientType;
12 |
13 | constructor(client: SupabaseClient, clientType: ClientType) {
14 | this.client = client;
15 | this.clientType = clientType;
16 |
17 | }
18 |
19 | async loginEmail(email: string, password: string) {
20 | return this.client.auth.signInWithPassword({
21 | email: email,
22 | password: password
23 | });
24 | }
25 |
26 | async registerEmail(email: string, password: string) {
27 | return this.client.auth.signUp({
28 | email: email,
29 | password: password
30 | });
31 | }
32 |
33 | async exchangeCodeForSession(code: string) {
34 | return this.client.auth.exchangeCodeForSession(code);
35 | }
36 |
37 | async resendVerificationEmail(email: string) {
38 | return this.client.auth.resend({
39 | email: email,
40 | type: 'signup'
41 | })
42 | }
43 |
44 | async logout() {
45 | const { error } = await this.client.auth.signOut({
46 | scope: 'local'
47 | });
48 | if (error) throw error;
49 | if(this.clientType === ClientType.SPA) {
50 | window.location.href = '/auth/login';
51 | }
52 | }
53 |
54 | async uploadFile(myId: string, filename: string, file: File) {
55 | filename = filename.replace(/[^0-9a-zA-Z!\-_.*'()]/g, '_');
56 | filename = myId + "/" + filename
57 | return this.client.storage.from('files').upload(filename, file);
58 | }
59 |
60 | async getFiles(myId: string) {
61 | return this.client.storage.from('files').list(myId)
62 | }
63 |
64 | async deleteFile(myId: string, filename: string) {
65 | filename = myId + "/" + filename
66 | return this.client.storage.from('files').remove([filename])
67 | }
68 |
69 | async shareFile(myId: string, filename: string, timeInSec: number, forDownload: boolean = false) {
70 | filename = myId + "/" + filename
71 | return this.client.storage.from('files').createSignedUrl(filename, timeInSec, {
72 | download: forDownload
73 | });
74 |
75 | }
76 |
77 | async getMyTodoList(page: number = 1, pageSize: number = 100, order: string = 'created_at', done: boolean | null = false) {
78 | let query = this.client.from('todo_list').select('*').range(page * pageSize - pageSize, page * pageSize - 1).order(order)
79 | if (done !== null) {
80 | query = query.eq('done', done)
81 | }
82 | return query
83 | }
84 |
85 | async createTask(row: Database["public"]["Tables"]["todo_list"]["Insert"]) {
86 | return this.client.from('todo_list').insert(row)
87 | }
88 |
89 | async removeTask (id: string) {
90 | return this.client.from('todo_list').delete().eq('id', id)
91 | }
92 |
93 | async updateAsDone (id: string) {
94 | return this.client.from('todo_list').update({done: true}).eq('id', id)
95 | }
96 |
97 | getSupabaseClient() {
98 | return this.client;
99 | }
100 |
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/nextjs/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export type Json =
2 | | string
3 | | number
4 | | boolean
5 | | null
6 | | { [key: string]: Json | undefined }
7 | | Json[]
8 |
9 | export type Database = {
10 | graphql_public: {
11 | Tables: {
12 | [_ in never]: never
13 | }
14 | Views: {
15 | [_ in never]: never
16 | }
17 | Functions: {
18 | graphql: {
19 | Args: {
20 | operationName?: string
21 | query?: string
22 | variables?: Json
23 | extensions?: Json
24 | }
25 | Returns: Json
26 | }
27 | }
28 | Enums: {
29 | [_ in never]: never
30 | }
31 | CompositeTypes: {
32 | [_ in never]: never
33 | }
34 | }
35 | public: {
36 | Tables: {
37 | todo_list: {
38 | Row: {
39 | created_at: string
40 | description: string | null
41 | done: boolean
42 | done_at: string | null
43 | id: number
44 | owner: string
45 | title: string
46 | urgent: boolean
47 | }
48 | Insert: {
49 | created_at?: string
50 | description?: string | null
51 | done?: boolean
52 | done_at?: string | null
53 | id?: number
54 | owner: string
55 | title: string
56 | urgent?: boolean
57 | }
58 | Update: {
59 | created_at?: string
60 | description?: string | null
61 | done?: boolean
62 | done_at?: string | null
63 | id?: number
64 | owner?: string
65 | title?: string
66 | urgent?: boolean
67 | }
68 | Relationships: []
69 | }
70 | }
71 | Views: {
72 | [_ in never]: never
73 | }
74 | Functions: {
75 | [_ in never]: never
76 | }
77 | Enums: {
78 | [_ in never]: never
79 | }
80 | CompositeTypes: {
81 | [_ in never]: never
82 | }
83 | }
84 | }
85 |
86 | type PublicSchema = Database[Extract]
87 |
88 | export type Tables<
89 | PublicTableNameOrOptions extends
90 | | keyof (PublicSchema["Tables"] & PublicSchema["Views"])
91 | | { schema: keyof Database },
92 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
93 | ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
94 | Database[PublicTableNameOrOptions["schema"]]["Views"])
95 | : never = never,
96 | > = PublicTableNameOrOptions extends { schema: keyof Database }
97 | ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
98 | Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
99 | Row: infer R
100 | }
101 | ? R
102 | : never
103 | : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
104 | PublicSchema["Views"])
105 | ? (PublicSchema["Tables"] &
106 | PublicSchema["Views"])[PublicTableNameOrOptions] extends {
107 | Row: infer R
108 | }
109 | ? R
110 | : never
111 | : never
112 |
113 | export type TablesInsert<
114 | PublicTableNameOrOptions extends
115 | | keyof PublicSchema["Tables"]
116 | | { schema: keyof Database },
117 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
118 | ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
119 | : never = never,
120 | > = PublicTableNameOrOptions extends { schema: keyof Database }
121 | ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
122 | Insert: infer I
123 | }
124 | ? I
125 | : never
126 | : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
127 | ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
128 | Insert: infer I
129 | }
130 | ? I
131 | : never
132 | : never
133 |
134 | export type TablesUpdate<
135 | PublicTableNameOrOptions extends
136 | | keyof PublicSchema["Tables"]
137 | | { schema: keyof Database },
138 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
139 | ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
140 | : never = never,
141 | > = PublicTableNameOrOptions extends { schema: keyof Database }
142 | ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
143 | Update: infer U
144 | }
145 | ? U
146 | : never
147 | : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
148 | ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
149 | Update: infer U
150 | }
151 | ? U
152 | : never
153 | : never
154 |
155 | export type Enums<
156 | PublicEnumNameOrOptions extends
157 | | keyof PublicSchema["Enums"]
158 | | { schema: keyof Database },
159 | EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
160 | ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
161 | : never = never,
162 | > = PublicEnumNameOrOptions extends { schema: keyof Database }
163 | ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
164 | : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
165 | ? PublicSchema["Enums"][PublicEnumNameOrOptions]
166 | : never
167 |
168 | export type CompositeTypes<
169 | PublicCompositeTypeNameOrOptions extends
170 | | keyof PublicSchema["CompositeTypes"]
171 | | { schema: keyof Database },
172 | CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
173 | schema: keyof Database
174 | }
175 | ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
176 | : never = never,
177 | > = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
178 | ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
179 | : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"]
180 | ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
181 | : never
182 |
--------------------------------------------------------------------------------
/nextjs/src/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 |
8 | export function generateRandomString(length = 8, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
9 | let result = '';
10 | const charsetLength = charset.length;
11 |
12 | for (let i = 0; i < length; i++) {
13 | result += charset.charAt(Math.floor(Math.random() * charsetLength));
14 | }
15 |
16 | return result;
17 | }
--------------------------------------------------------------------------------
/nextjs/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from 'next/server'
2 | import { updateSession } from '@/lib/supabase/middleware'
3 |
4 | export async function middleware(request: NextRequest) {
5 | return await updateSession(request)
6 | }
7 |
8 | export const config = {
9 | matcher: [
10 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
11 | ],
12 | }
--------------------------------------------------------------------------------
/nextjs/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | primary: {
14 | '50': 'var(--color-primary-50)',
15 | '100': 'var(--color-primary-100)',
16 | '200': 'var(--color-primary-200)',
17 | '300': 'var(--color-primary-300)',
18 | '400': 'var(--color-primary-400)',
19 | '500': 'var(--color-primary-500)',
20 | '600': 'var(--color-primary-600)',
21 | '700': 'var(--color-primary-700)',
22 | '800': 'var(--color-primary-800)',
23 | '900': 'var(--color-primary-900)',
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | '50': 'var(--color-secondary-50)',
29 | '100': 'var(--color-secondary-100)',
30 | '200': 'var(--color-secondary-200)',
31 | '300': 'var(--color-secondary-300)',
32 | '400': 'var(--color-secondary-400)',
33 | '500': 'var(--color-secondary-500)',
34 | '600': 'var(--color-secondary-600)',
35 | '700': 'var(--color-secondary-700)',
36 | '800': 'var(--color-secondary-800)',
37 | '900': 'var(--color-secondary-900)',
38 | DEFAULT: 'hsl(var(--secondary))',
39 | foreground: 'hsl(var(--secondary-foreground))'
40 | },
41 | background: 'hsl(var(--background))',
42 | foreground: 'hsl(var(--foreground))',
43 | card: {
44 | DEFAULT: 'hsl(var(--card))',
45 | foreground: 'hsl(var(--card-foreground))'
46 | },
47 | popover: {
48 | DEFAULT: 'hsl(var(--popover))',
49 | foreground: 'hsl(var(--popover-foreground))'
50 | },
51 | muted: {
52 | DEFAULT: 'hsl(var(--muted))',
53 | foreground: 'hsl(var(--muted-foreground))'
54 | },
55 | accent: {
56 | DEFAULT: 'hsl(var(--accent))',
57 | foreground: 'hsl(var(--accent-foreground))'
58 | },
59 | destructive: {
60 | DEFAULT: 'hsl(var(--destructive))',
61 | foreground: 'hsl(var(--destructive-foreground))'
62 | },
63 | border: 'hsl(var(--border))',
64 | input: 'hsl(var(--input))',
65 | ring: 'hsl(var(--ring))',
66 | chart: {
67 | '1': 'hsl(var(--chart-1))',
68 | '2': 'hsl(var(--chart-2))',
69 | '3': 'hsl(var(--chart-3))',
70 | '4': 'hsl(var(--chart-4))',
71 | '5': 'hsl(var(--chart-5))'
72 | }
73 | },
74 | borderRadius: {
75 | lg: 'var(--radius)',
76 | md: 'calc(var(--radius) - 2px)',
77 | sm: 'calc(var(--radius) - 4px)'
78 | }
79 | }
80 | },
81 | plugins: [require("tailwindcss-animate")],
82 | };
83 |
84 | export default config;
--------------------------------------------------------------------------------
/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # For detailed configuration reference documentation, visit:
2 | # https://supabase.com/docs/guides/local-development/cli/config
3 | # A string used to distinguish different Supabase projects on the same host. Defaults to the
4 | # working directory name when running `supabase init`.
5 | project_id = "sasstemplate"
6 |
7 | [api]
8 | enabled = true
9 | # Port to use for the API URL.
10 | port = 54321
11 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
12 | # endpoints. `public` and `graphql_public` schemas are included by default.
13 | schemas = ["public", "graphql_public"]
14 | # Extra schemas to add to the search_path of every request.
15 | extra_search_path = ["public", "extensions"]
16 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
17 | # for accidental or malicious requests.
18 | max_rows = 1000
19 |
20 | [api.tls]
21 | # Enable HTTPS endpoints locally using a self-signed certificate.
22 | enabled = false
23 |
24 | [db]
25 | # Port to use for the local database URL.
26 | port = 54322
27 | # Port used by db diff command to initialize the shadow database.
28 | shadow_port = 54320
29 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
30 | # server_version;` on the remote database to check.
31 | major_version = 15
32 |
33 | [db.pooler]
34 | enabled = false
35 | # Port to use for the local connection pooler.
36 | port = 54329
37 | # Specifies when a server connection can be reused by other clients.
38 | # Configure one of the supported pooler modes: `transaction`, `session`.
39 | pool_mode = "transaction"
40 | # How many server connections to allow per user/database pair.
41 | default_pool_size = 20
42 | # Maximum number of client connections allowed.
43 | max_client_conn = 100
44 |
45 | [db.seed]
46 | # If enabled, seeds the database after migrations during a db reset.
47 | enabled = true
48 | # Specifies an ordered list of seed files to load during db reset.
49 | # Supports glob patterns relative to supabase directory: './seeds/*.sql'
50 | sql_paths = ['./seed.sql']
51 |
52 | [realtime]
53 | enabled = true
54 | # Bind realtime via either IPv4 or IPv6. (default: IPv4)
55 | # ip_version = "IPv6"
56 | # The maximum length in bytes of HTTP request headers. (default: 4096)
57 | # max_header_length = 4096
58 |
59 | [studio]
60 | enabled = true
61 | # Port to use for Supabase Studio.
62 | port = 54323
63 | # External URL of the API server that frontend connects to.
64 | api_url = "http://127.0.0.1"
65 | # OpenAI API Key to use for Supabase AI in the Supabase Studio.
66 | openai_api_key = "env(OPENAI_API_KEY)"
67 |
68 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
69 | # are monitored, and you can view the emails that would have been sent from the web interface.
70 | [inbucket]
71 | enabled = true
72 | # Port to use for the email testing server web interface.
73 | port = 54324
74 | # Uncomment to expose additional ports for testing user applications that send emails.
75 | # smtp_port = 54325
76 | # pop3_port = 54326
77 | # admin_email = "admin@email.com"
78 | # sender_name = "Admin"
79 |
80 | [storage]
81 | enabled = true
82 | # The maximum file size allowed (e.g. "5MB", "500KB").
83 | file_size_limit = "50MiB"
84 |
85 | # Image transformation API is available to Supabase Pro plan.
86 | # [storage.image_transformation]
87 | # enabled = true
88 |
89 | # Uncomment to configure local storage buckets
90 | # [storage.buckets.images]
91 | # public = false
92 | # file_size_limit = "50MiB"
93 | # allowed_mime_types = ["image/png", "image/jpeg"]
94 | # objects_path = "./images"
95 |
96 | [auth]
97 | enabled = true
98 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
99 | # in emails.
100 | site_url = "http://localhost:3000"
101 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
102 | additional_redirect_urls = ["https://127.0.0.1:3000/**", "http://localhost:3000/**", "https://localhost:3000/**"]
103 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
104 | jwt_expiry = 3600
105 | # If disabled, the refresh token will never expire.
106 | enable_refresh_token_rotation = true
107 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
108 | # Requires enable_refresh_token_rotation = true.
109 | refresh_token_reuse_interval = 10
110 | # Allow/disallow new user signups to your project.
111 | enable_signup = true
112 | # Allow/disallow anonymous sign-ins to your project.
113 | enable_anonymous_sign_ins = false
114 | # Allow/disallow testing manual linking of accounts
115 | enable_manual_linking = false
116 | # Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
117 | minimum_password_length = 6
118 | # Passwords that do not meet the following requirements will be rejected as weak. Supported values
119 | # are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
120 | password_requirements = ""
121 |
122 | [auth.email]
123 | # Allow/disallow new user signups via email to your project.
124 | enable_signup = true
125 | # If enabled, a user will be required to confirm any email change on both the old, and new email
126 | # addresses. If disabled, only the new email is required to confirm.
127 | double_confirm_changes = true
128 | # If enabled, users need to confirm their email address before signing in.
129 | enable_confirmations = true
130 | # If enabled, users will need to reauthenticate or have logged in recently to change their password.
131 | secure_password_change = false
132 | # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
133 | max_frequency = "1m0s"
134 | # Number of characters used in the email OTP.
135 | otp_length = 6
136 | # Number of seconds before the email OTP expires (defaults to 1 hour).
137 | otp_expiry = 3600
138 |
139 | # Use a production-ready SMTP server
140 | # [auth.email.smtp]
141 | # enabled = true
142 | # host = "smtp.sendgrid.net"
143 | # port = 587
144 | # user = "apikey"
145 | # pass = "env(SENDGRID_API_KEY)"
146 | # admin_email = "admin@email.com"
147 | # sender_name = "Admin"
148 |
149 | # Uncomment to customize email template
150 | # [auth.email.template.invite]
151 | # subject = "You have been invited"
152 | # content_path = "./supabase/templates/invite.html"
153 |
154 | [auth.sms]
155 | # Allow/disallow new user signups via SMS to your project.
156 | enable_signup = false
157 | # If enabled, users need to confirm their phone number before signing in.
158 | enable_confirmations = false
159 | # Template for sending OTP to users
160 | template = "Your code is {{ .Code }}"
161 | # Controls the minimum amount of time that must pass before sending another sms otp.
162 | max_frequency = "5s"
163 |
164 | # Use pre-defined map of phone number to OTP for testing.
165 | # [auth.sms.test_otp]
166 | # 4152127777 = "123456"
167 |
168 | # Configure logged in session timeouts.
169 | # [auth.sessions]
170 | # Force log out after the specified duration.
171 | # timebox = "24h"
172 | # Force log out if the user has been inactive longer than the specified duration.
173 | # inactivity_timeout = "8h"
174 |
175 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
176 | # [auth.hook.custom_access_token]
177 | # enabled = true
178 | # uri = "pg-functions:////"
179 |
180 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
181 | [auth.sms.twilio]
182 | enabled = false
183 | account_sid = ""
184 | message_service_sid = ""
185 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
186 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
187 |
188 | # Multi-factor-authentication is available to Supabase Pro plan.
189 | [auth.mfa]
190 | # Control how many MFA factors can be enrolled at once per user.
191 | max_enrolled_factors = 10
192 |
193 | # Control MFA via App Authenticator (TOTP)
194 | [auth.mfa.totp]
195 | enroll_enabled = true
196 | verify_enabled = true
197 |
198 | # Configure MFA via Phone Messaging
199 | [auth.mfa.phone]
200 | enroll_enabled = false
201 | verify_enabled = false
202 | otp_length = 6
203 | template = "Your code is {{ .Code }}"
204 | max_frequency = "5s"
205 |
206 | # Configure MFA via WebAuthn
207 | # [auth.mfa.web_authn]
208 | # enroll_enabled = true
209 | # verify_enabled = true
210 |
211 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
212 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
213 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
214 | [auth.external.apple]
215 | enabled = false
216 | client_id = ""
217 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
218 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
219 | # Overrides the default auth redirectUrl.
220 | redirect_uri = ""
221 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
222 | # or any other third-party OIDC providers.
223 | url = ""
224 | # If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
225 | skip_nonce_check = false
226 |
227 | # Use Firebase Auth as a third-party provider alongside Supabase Auth.
228 | [auth.third_party.firebase]
229 | enabled = false
230 | # project_id = "my-firebase-project"
231 |
232 | # Use Auth0 as a third-party provider alongside Supabase Auth.
233 | [auth.third_party.auth0]
234 | enabled = false
235 | # tenant = "my-auth0-tenant"
236 | # tenant_region = "us"
237 |
238 | # Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
239 | [auth.third_party.aws_cognito]
240 | enabled = false
241 | # user_pool_id = "my-user-pool-id"
242 | # user_pool_region = "us-east-1"
243 |
244 | [edge_runtime]
245 | enabled = true
246 | # Configure one of the supported request policies: `oneshot`, `per_worker`.
247 | # Use `oneshot` for hot reload, or `per_worker` for load testing.
248 | policy = "oneshot"
249 | # Port to attach the Chrome inspector for debugging edge functions.
250 | inspector_port = 8083
251 |
252 | # Use these configurations to customize your Edge Function.
253 | # [functions.MY_FUNCTION_NAME]
254 | # enabled = true
255 | # verify_jwt = true
256 | # import_map = "./functions/MY_FUNCTION_NAME/deno.json"
257 | # Uncomment to specify a custom file path to the entrypoint.
258 | # Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
259 | # entrypoint = "./functions/MY_FUNCTION_NAME/index.ts"
260 |
261 | [analytics]
262 | enabled = true
263 | port = 54327
264 | # Configure one of the supported backends: `postgres`, `bigquery`.
265 | backend = "postgres"
266 |
267 | # Experimental features may be deprecated any time
268 | [experimental]
269 | # Configures Postgres storage engine to use OrioleDB (S3)
270 | orioledb_version = ""
271 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com
272 | s3_host = "env(S3_HOST)"
273 | # Configures S3 bucket region, eg. us-east-1
274 | s3_region = "env(S3_REGION)"
275 | # Configures AWS_ACCESS_KEY_ID for S3 bucket
276 | s3_access_key = "env(S3_ACCESS_KEY)"
277 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket
278 | s3_secret_key = "env(S3_SECRET_KEY)"
279 |
--------------------------------------------------------------------------------
/supabase/migrations/20250107210416_MFA.sql:
--------------------------------------------------------------------------------
1 | create schema if not exists "authenticative";
2 |
3 | set check_function_bodies = off;
4 |
5 | CREATE OR REPLACE FUNCTION authenticative.is_user_authenticated()
6 | RETURNS boolean
7 | LANGUAGE sql
8 | SECURITY DEFINER
9 | AS $function$
10 | SELECT array[(select auth.jwt()->>'aal')] <@ (
11 | SELECT
12 | CASE
13 | WHEN count(id) > 0 THEN array['aal2']
14 | ELSE array['aal1', 'aal2']
15 | END as aal
16 | FROM auth.mfa_factors
17 | WHERE (auth.uid() = user_id)
18 | AND status = 'verified'
19 | );
20 | $function$
21 | ;
22 |
23 |
--------------------------------------------------------------------------------
/supabase/migrations/20250130165844_example_storage.sql:
--------------------------------------------------------------------------------
1 | insert into storage.buckets
2 | (id, name, public)
3 | values
4 | ('files', 'files', false);
--------------------------------------------------------------------------------
/supabase/migrations/20250130181110_storage_policies.sql:
--------------------------------------------------------------------------------
1 | create policy "Give users access to own folder 1m0cqf_0"
2 | on "storage"."objects"
3 | as permissive
4 | for delete
5 | to public
6 | using (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
7 |
8 |
9 | create policy "Give users access to own folder 1m0cqf_1"
10 | on "storage"."objects"
11 | as permissive
12 | for update
13 | to public
14 | using (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
15 |
16 |
17 | create policy "Give users access to own folder 1m0cqf_2"
18 | on "storage"."objects"
19 | as permissive
20 | for insert
21 | to public
22 | with check (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
23 |
24 |
25 | create policy "Give users access to own folder 1m0cqf_3"
26 | on "storage"."objects"
27 | as permissive
28 | for select
29 | to public
30 | using (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/supabase/migrations/20250130181641_todo_list.sql:
--------------------------------------------------------------------------------
1 | create table "public"."todo_list" (
2 | "id" bigint generated by default as identity not null,
3 | "created_at" timestamp with time zone not null default now(),
4 | "title" text not null,
5 | "urgent" boolean not null default false,
6 | "description" text,
7 | "done" boolean not null default false,
8 | "done_at" timestamp with time zone,
9 | "owner" uuid not null
10 | );
11 |
12 |
13 | alter table "public"."todo_list" enable row level security;
14 |
15 | CREATE UNIQUE INDEX todo_list_pkey ON public.todo_list USING btree (id);
16 |
17 | alter table "public"."todo_list" add constraint "todo_list_pkey" PRIMARY KEY using index "todo_list_pkey";
18 |
19 | alter table "public"."todo_list" add constraint "todo_list_owner_fkey" FOREIGN KEY (owner) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
20 |
21 | alter table "public"."todo_list" validate constraint "todo_list_owner_fkey";
22 |
23 | grant delete on table "public"."todo_list" to "anon";
24 |
25 | grant insert on table "public"."todo_list" to "anon";
26 |
27 | grant references on table "public"."todo_list" to "anon";
28 |
29 | grant select on table "public"."todo_list" to "anon";
30 |
31 | grant trigger on table "public"."todo_list" to "anon";
32 |
33 | grant truncate on table "public"."todo_list" to "anon";
34 |
35 | grant update on table "public"."todo_list" to "anon";
36 |
37 | grant delete on table "public"."todo_list" to "authenticated";
38 |
39 | grant insert on table "public"."todo_list" to "authenticated";
40 |
41 | grant references on table "public"."todo_list" to "authenticated";
42 |
43 | grant select on table "public"."todo_list" to "authenticated";
44 |
45 | grant trigger on table "public"."todo_list" to "authenticated";
46 |
47 | grant truncate on table "public"."todo_list" to "authenticated";
48 |
49 | grant update on table "public"."todo_list" to "authenticated";
50 |
51 | grant delete on table "public"."todo_list" to "service_role";
52 |
53 | grant insert on table "public"."todo_list" to "service_role";
54 |
55 | grant references on table "public"."todo_list" to "service_role";
56 |
57 | grant select on table "public"."todo_list" to "service_role";
58 |
59 | grant trigger on table "public"."todo_list" to "service_role";
60 |
61 | grant truncate on table "public"."todo_list" to "service_role";
62 |
63 | grant update on table "public"."todo_list" to "service_role";
64 |
65 | create policy "Owner can do everything"
66 | on "public"."todo_list"
67 | as permissive
68 | for all
69 | to authenticated
70 | using ((authenticative.is_user_authenticated() AND (owner = auth.uid())));
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/supabase/migrations_for_old/20250107210416_MFA.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION auth.is_user_authenticated()
2 | RETURNS boolean
3 | LANGUAGE sql
4 | SECURITY DEFINER
5 | AS $$
6 | SELECT array[(select auth.jwt()->>'aal')] <@ (
7 | SELECT
8 | CASE
9 | WHEN count(id) > 0 THEN array['aal2']
10 | ELSE array['aal1', 'aal2']
11 | END as aal
12 | FROM auth.mfa_factors
13 | WHERE (auth.uid() = user_id)
14 | AND status = 'verified'
15 | );
16 | $$;
17 |
--------------------------------------------------------------------------------
/supabase/migrations_for_old/20250130165844_example_storage.sql:
--------------------------------------------------------------------------------
1 | insert into storage.buckets
2 | (id, name, public)
3 | values
4 | ('files', 'files', false);
--------------------------------------------------------------------------------
/supabase/migrations_for_old/20250130181110_storage_policies.sql:
--------------------------------------------------------------------------------
1 | create policy "Give users access to own folder 1m0cqf_0"
2 | on "storage"."objects"
3 | as permissive
4 | for delete
5 | to public
6 | using (((bucket_id = 'files'::text) AND auth.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
7 |
8 |
9 | create policy "Give users access to own folder 1m0cqf_1"
10 | on "storage"."objects"
11 | as permissive
12 | for update
13 | to public
14 | using (((bucket_id = 'files'::text) AND auth.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
15 |
16 |
17 | create policy "Give users access to own folder 1m0cqf_2"
18 | on "storage"."objects"
19 | as permissive
20 | for insert
21 | to public
22 | with check (((bucket_id = 'files'::text) AND auth.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
23 |
24 |
25 | create policy "Give users access to own folder 1m0cqf_3"
26 | on "storage"."objects"
27 | as permissive
28 | for select
29 | to public
30 | using (((bucket_id = 'files'::text) AND auth.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/supabase/migrations_for_old/20250130181641_todo_list.sql:
--------------------------------------------------------------------------------
1 | create table "public"."todo_list" (
2 | "id" bigint generated by default as identity not null,
3 | "created_at" timestamp with time zone not null default now(),
4 | "title" text not null,
5 | "urgent" boolean not null default false,
6 | "description" text,
7 | "done" boolean not null default false,
8 | "done_at" timestamp with time zone,
9 | "owner" uuid not null
10 | );
11 |
12 |
13 | alter table "public"."todo_list" enable row level security;
14 |
15 | CREATE UNIQUE INDEX todo_list_pkey ON public.todo_list USING btree (id);
16 |
17 | alter table "public"."todo_list" add constraint "todo_list_pkey" PRIMARY KEY using index "todo_list_pkey";
18 |
19 | alter table "public"."todo_list" add constraint "todo_list_owner_fkey" FOREIGN KEY (owner) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
20 |
21 | alter table "public"."todo_list" validate constraint "todo_list_owner_fkey";
22 |
23 | grant delete on table "public"."todo_list" to "anon";
24 |
25 | grant insert on table "public"."todo_list" to "anon";
26 |
27 | grant references on table "public"."todo_list" to "anon";
28 |
29 | grant select on table "public"."todo_list" to "anon";
30 |
31 | grant trigger on table "public"."todo_list" to "anon";
32 |
33 | grant truncate on table "public"."todo_list" to "anon";
34 |
35 | grant update on table "public"."todo_list" to "anon";
36 |
37 | grant delete on table "public"."todo_list" to "authenticated";
38 |
39 | grant insert on table "public"."todo_list" to "authenticated";
40 |
41 | grant references on table "public"."todo_list" to "authenticated";
42 |
43 | grant select on table "public"."todo_list" to "authenticated";
44 |
45 | grant trigger on table "public"."todo_list" to "authenticated";
46 |
47 | grant truncate on table "public"."todo_list" to "authenticated";
48 |
49 | grant update on table "public"."todo_list" to "authenticated";
50 |
51 | grant delete on table "public"."todo_list" to "service_role";
52 |
53 | grant insert on table "public"."todo_list" to "service_role";
54 |
55 | grant references on table "public"."todo_list" to "service_role";
56 |
57 | grant select on table "public"."todo_list" to "service_role";
58 |
59 | grant trigger on table "public"."todo_list" to "service_role";
60 |
61 | grant truncate on table "public"."todo_list" to "service_role";
62 |
63 | grant update on table "public"."todo_list" to "service_role";
64 |
65 | create policy "Owner can do everything"
66 | on "public"."todo_list"
67 | as permissive
68 | for all
69 | to authenticated
70 | using ((auth.is_user_authenticated() AND (owner = auth.uid())));
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/supabase/migrations_for_old/20250525183944_auth_removal.sql:
--------------------------------------------------------------------------------
1 | create schema if not exists "authenticative";
2 |
3 | set check_function_bodies = off;
4 |
5 | CREATE OR REPLACE FUNCTION authenticative.is_user_authenticated()
6 | RETURNS boolean
7 | LANGUAGE sql
8 | SECURITY DEFINER
9 | AS $function$
10 | SELECT array[(select auth.jwt()->>'aal')] <@ (
11 | SELECT
12 | CASE
13 | WHEN count(id) > 0 THEN array['aal2']
14 | ELSE array['aal1', 'aal2']
15 | END as aal
16 | FROM auth.mfa_factors
17 | WHERE (auth.uid() = user_id)
18 | AND status = 'verified'
19 | );
20 | $function$
21 | ;
22 |
23 |
24 | drop policy "Owner can do everything" on "public"."todo_list";
25 |
26 | create policy "Owner can do everything"
27 | on "public"."todo_list"
28 | as permissive
29 | for all
30 | to authenticated
31 | using ((authenticative.is_user_authenticated() AND (owner = auth.uid())));
32 |
33 |
34 |
35 | drop policy "Give users access to own folder 1m0cqf_0" on "storage"."objects";
36 |
37 | drop policy "Give users access to own folder 1m0cqf_1" on "storage"."objects";
38 |
39 | drop policy "Give users access to own folder 1m0cqf_2" on "storage"."objects";
40 |
41 | drop policy "Give users access to own folder 1m0cqf_3" on "storage"."objects";
42 |
43 | create policy "Give users access to own folder 1m0cqf_0"
44 | on "storage"."objects"
45 | as permissive
46 | for delete
47 | to public
48 | using (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
49 |
50 |
51 | create policy "Give users access to own folder 1m0cqf_1"
52 | on "storage"."objects"
53 | as permissive
54 | for update
55 | to public
56 | using (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
57 |
58 |
59 | create policy "Give users access to own folder 1m0cqf_2"
60 | on "storage"."objects"
61 | as permissive
62 | for insert
63 | to public
64 | with check (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
65 |
66 |
67 | create policy "Give users access to own folder 1m0cqf_3"
68 | on "storage"."objects"
69 | as permissive
70 | for select
71 | to public
72 | using (((bucket_id = 'files'::text) AND authenticative.is_user_authenticated() AND (name ~ (('^'::text || (auth.uid())::text) || '/'::text))));
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------