nobalmako 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/components.json +22 -0
- package/dist/nobalmako.js +272 -0
- package/drizzle/0000_pink_spiral.sql +126 -0
- package/drizzle/meta/0000_snapshot.json +1027 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/index.ts +118 -0
- package/src/app/api/api-keys/[id]/route.ts +147 -0
- package/src/app/api/api-keys/route.ts +151 -0
- package/src/app/api/audit-logs/route.ts +84 -0
- package/src/app/api/auth/forgot-password/route.ts +47 -0
- package/src/app/api/auth/login/route.ts +99 -0
- package/src/app/api/auth/logout/route.ts +15 -0
- package/src/app/api/auth/me/route.ts +23 -0
- package/src/app/api/auth/mfa/setup/route.ts +33 -0
- package/src/app/api/auth/mfa/verify/route.ts +45 -0
- package/src/app/api/auth/register/route.ts +140 -0
- package/src/app/api/auth/reset-password/route.ts +52 -0
- package/src/app/api/auth/update/route.ts +71 -0
- package/src/app/api/auth/verify/route.ts +39 -0
- package/src/app/api/environments/route.ts +227 -0
- package/src/app/api/team-members/route.ts +385 -0
- package/src/app/api/teams/route.ts +217 -0
- package/src/app/api/variable-history/route.ts +218 -0
- package/src/app/api/variables/route.ts +476 -0
- package/src/app/api/webhooks/route.ts +77 -0
- package/src/app/api-keys/APIKeysClient.tsx +316 -0
- package/src/app/api-keys/page.tsx +10 -0
- package/src/app/api-reference/page.tsx +324 -0
- package/src/app/audit-log/AuditLogClient.tsx +229 -0
- package/src/app/audit-log/page.tsx +10 -0
- package/src/app/auth/forgot-password/page.tsx +121 -0
- package/src/app/auth/login/LoginForm.tsx +145 -0
- package/src/app/auth/login/page.tsx +11 -0
- package/src/app/auth/register/RegisterForm.tsx +156 -0
- package/src/app/auth/register/page.tsx +16 -0
- package/src/app/auth/reset-password/page.tsx +160 -0
- package/src/app/dashboard/DashboardClient.tsx +219 -0
- package/src/app/dashboard/page.tsx +11 -0
- package/src/app/docs/page.tsx +251 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +231 -0
- package/src/app/profile/ProfileClient.tsx +230 -0
- package/src/app/profile/page.tsx +10 -0
- package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
- package/src/app/project/[id]/page.tsx +17 -0
- package/src/bin/nobalmako.ts +341 -0
- package/src/components/ApiKeysManager.tsx +529 -0
- package/src/components/AppLayout.tsx +193 -0
- package/src/components/BulkActions.tsx +138 -0
- package/src/components/CreateEnvironmentDialog.tsx +207 -0
- package/src/components/CreateTeamDialog.tsx +174 -0
- package/src/components/CreateVariableDialog.tsx +311 -0
- package/src/components/DeleteEnvironmentDialog.tsx +104 -0
- package/src/components/DeleteTeamDialog.tsx +112 -0
- package/src/components/DeleteVariableDialog.tsx +103 -0
- package/src/components/EditEnvironmentDialog.tsx +202 -0
- package/src/components/EditMemberDialog.tsx +143 -0
- package/src/components/EditTeamDialog.tsx +178 -0
- package/src/components/EditVariableDialog.tsx +231 -0
- package/src/components/ImportVariablesDialog.tsx +347 -0
- package/src/components/InviteMemberDialog.tsx +191 -0
- package/src/components/LeaveProjectDialog.tsx +111 -0
- package/src/components/MFASettings.tsx +136 -0
- package/src/components/ProjectDiff.tsx +123 -0
- package/src/components/Providers.tsx +24 -0
- package/src/components/RemoveMemberDialog.tsx +112 -0
- package/src/components/SearchDialog.tsx +276 -0
- package/src/components/SecurityOverview.tsx +92 -0
- package/src/components/TeamMembersManager.tsx +103 -0
- package/src/components/VariableHistoryDialog.tsx +265 -0
- package/src/components/WebhooksManager.tsx +169 -0
- package/src/components/ui/alert-dialog.tsx +160 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/use-api-keys.ts +95 -0
- package/src/hooks/use-audit-logs.ts +58 -0
- package/src/hooks/use-auth.tsx +121 -0
- package/src/hooks/use-environments.ts +33 -0
- package/src/hooks/use-project-permissions.ts +49 -0
- package/src/hooks/use-team-members.ts +30 -0
- package/src/hooks/use-teams.ts +33 -0
- package/src/hooks/use-variables.ts +38 -0
- package/src/lib/audit.ts +36 -0
- package/src/lib/auth.ts +108 -0
- package/src/lib/crypto.ts +39 -0
- package/src/lib/db.ts +15 -0
- package/src/lib/dynamic-providers.ts +19 -0
- package/src/lib/email.ts +110 -0
- package/src/lib/mail.ts +51 -0
- package/src/lib/permissions.ts +51 -0
- package/src/lib/schema.ts +240 -0
- package/src/lib/seed.ts +107 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhooks.ts +42 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-background: hsl(var(--background));
|
|
5
|
+
--color-foreground: hsl(var(--foreground));
|
|
6
|
+
--color-card: hsl(var(--card));
|
|
7
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
8
|
+
--color-popover: hsl(var(--popover));
|
|
9
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
10
|
+
--color-primary: hsl(var(--primary));
|
|
11
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
12
|
+
--color-secondary: hsl(var(--secondary));
|
|
13
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
14
|
+
--color-muted: hsl(var(--muted));
|
|
15
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
16
|
+
--color-accent: hsl(var(--accent));
|
|
17
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
18
|
+
--color-destructive: hsl(var(--destructive));
|
|
19
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
20
|
+
--color-border: hsl(var(--border));
|
|
21
|
+
--color-input: hsl(var(--input));
|
|
22
|
+
--color-ring: hsl(var(--ring));
|
|
23
|
+
--radius-xl: var(--radius);
|
|
24
|
+
--radius-lg: calc(var(--radius) - 2px);
|
|
25
|
+
--radius-md: calc(var(--radius) - 4px);
|
|
26
|
+
--radius-sm: calc(var(--radius) - 6px);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:root {
|
|
30
|
+
color-scheme: light;
|
|
31
|
+
--background: 0 0% 100%;
|
|
32
|
+
--foreground: 222.2 84% 4.9%;
|
|
33
|
+
--card: 0 0% 100%;
|
|
34
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
35
|
+
--popover: 0 0% 100%;
|
|
36
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
37
|
+
--primary: 221.2 83.2% 53.3%;
|
|
38
|
+
--primary-foreground: 210 40% 98%;
|
|
39
|
+
--secondary: 210 40% 96.1%;
|
|
40
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
41
|
+
--muted: 210 40% 96.1%;
|
|
42
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
43
|
+
--accent: 210 40% 96.1%;
|
|
44
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
45
|
+
--destructive: 0 84.2% 60.2%;
|
|
46
|
+
--destructive-foreground: 210 40% 98%;
|
|
47
|
+
--border: 214.3 31.8% 91.4%;
|
|
48
|
+
--input: 214.3 31.8% 91.4%;
|
|
49
|
+
--ring: 221.2 83.2% 53.3%;
|
|
50
|
+
--radius: 0.5rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Base styles */
|
|
54
|
+
*, ::after, ::before {
|
|
55
|
+
border-color: hsl(var(--border));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
body {
|
|
59
|
+
color: hsl(var(--foreground));
|
|
60
|
+
background-color: hsl(var(--background)) !important;
|
|
61
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
62
|
+
line-height: 1.6;
|
|
63
|
+
-webkit-font-smoothing: antialiased;
|
|
64
|
+
-moz-osx-font-smoothing: grayscale;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Typography */
|
|
68
|
+
h1, h2, h3, h4, h5, h6 {
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
line-height: 1.25;
|
|
71
|
+
letter-spacing: -0.025em;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
h1 {
|
|
75
|
+
font-size: 2.25rem;
|
|
76
|
+
font-weight: 700;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
h2 {
|
|
80
|
+
font-size: 1.875rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
h3 {
|
|
84
|
+
font-size: 1.5rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
h4 {
|
|
88
|
+
font-size: 1.25rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
p {
|
|
92
|
+
color: hsl(var(--muted-foreground));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Code blocks */
|
|
96
|
+
code {
|
|
97
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Cascadia Code', monospace;
|
|
98
|
+
font-size: 0.875em;
|
|
99
|
+
background: hsl(var(--muted));
|
|
100
|
+
padding: 0.125rem 0.25rem;
|
|
101
|
+
border-radius: 0.25rem;
|
|
102
|
+
border: 1px solid hsl(var(--border));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pre {
|
|
106
|
+
background: hsl(var(--muted));
|
|
107
|
+
border: 1px solid hsl(var(--border));
|
|
108
|
+
border-radius: var(--radius);
|
|
109
|
+
padding: 1rem;
|
|
110
|
+
overflow-x: auto;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pre code {
|
|
114
|
+
background: none;
|
|
115
|
+
padding: 0;
|
|
116
|
+
border: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Focus styles */
|
|
120
|
+
.focus-visible:focus-visible {
|
|
121
|
+
outline: 2px solid hsl(var(--ring));
|
|
122
|
+
outline-offset: 2px;
|
|
123
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
5
|
+
import { AuthProvider } from "@/hooks/use-auth";
|
|
6
|
+
import { Providers } from "@/components/Providers";
|
|
7
|
+
|
|
8
|
+
const inter = Inter({
|
|
9
|
+
subsets: ["latin"],
|
|
10
|
+
variable: "--font-inter",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const metadata: Metadata = {
|
|
14
|
+
title: "Nobalmako - Environment Variable Management",
|
|
15
|
+
description: "Secure, collaborative environment variable management platform",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function RootLayout({
|
|
19
|
+
children,
|
|
20
|
+
}: Readonly<{
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}>) {
|
|
23
|
+
return (
|
|
24
|
+
<html lang="en" className={inter.variable} suppressHydrationWarning>
|
|
25
|
+
<body className="font-sans antialiased">
|
|
26
|
+
<Providers>
|
|
27
|
+
<AuthProvider>
|
|
28
|
+
{children}
|
|
29
|
+
<Toaster />
|
|
30
|
+
</AuthProvider>
|
|
31
|
+
</Providers>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
+
import { Shield, Users, Database, Search, Download, Upload, ArrowRight, CheckCircle } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="min-h-screen bg-background">
|
|
9
|
+
{/* Header */}
|
|
10
|
+
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
11
|
+
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
|
12
|
+
<div className="flex items-center space-x-3">
|
|
13
|
+
<Shield className="h-8 w-8 text-primary" />
|
|
14
|
+
<div>
|
|
15
|
+
<span className="text-2xl font-bold">Nobalmako</span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<nav className="hidden md:flex items-center space-x-8">
|
|
19
|
+
<a href="#features" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
20
|
+
Features
|
|
21
|
+
</a>
|
|
22
|
+
<Link href="/docs" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
23
|
+
Docs
|
|
24
|
+
</Link>
|
|
25
|
+
</nav>
|
|
26
|
+
<div className="flex items-center space-x-3">
|
|
27
|
+
<Button variant="ghost" size="sm" asChild>
|
|
28
|
+
<Link href="/auth/login">Sign in</Link>
|
|
29
|
+
</Button>
|
|
30
|
+
<Button size="sm" asChild>
|
|
31
|
+
<Link href="/auth/register">Get Started</Link>
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
{/* Hero Section */}
|
|
38
|
+
<section className="py-24 lg:py-32">
|
|
39
|
+
<div className="container mx-auto px-4">
|
|
40
|
+
<div className="max-w-4xl mx-auto text-center">
|
|
41
|
+
<div className="inline-flex items-center px-4 py-2 rounded-full bg-muted mb-8">
|
|
42
|
+
<CheckCircle className="h-4 w-4 text-primary mr-2" />
|
|
43
|
+
<span className="text-sm font-medium">
|
|
44
|
+
Trusted by developers worldwide
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<h1 className="text-4xl lg:text-6xl font-bold mb-6">
|
|
49
|
+
Secure Environment
|
|
50
|
+
<br />
|
|
51
|
+
Variable Management
|
|
52
|
+
</h1>
|
|
53
|
+
|
|
54
|
+
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
|
55
|
+
Manage your environment variables securely across all your projects and environments.
|
|
56
|
+
Collaborate with your team while keeping sensitive data protected.
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
|
|
60
|
+
<Button size="lg" asChild>
|
|
61
|
+
<Link href="/auth/register">
|
|
62
|
+
Get Started for Free
|
|
63
|
+
<ArrowRight className="ml-2 h-5 w-5" />
|
|
64
|
+
</Link>
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Trust indicators */}
|
|
69
|
+
<div className="flex flex-wrap justify-center items-center gap-8 text-sm text-muted-foreground">
|
|
70
|
+
<div className="flex items-center">
|
|
71
|
+
<CheckCircle className="h-4 w-4 text-primary mr-2" />
|
|
72
|
+
SOC 2 Compliant
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex items-center">
|
|
75
|
+
<CheckCircle className="h-4 w-4 text-primary mr-2" />
|
|
76
|
+
End-to-end encrypted
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-center">
|
|
79
|
+
<CheckCircle className="h-4 w-4 text-primary mr-2" />
|
|
80
|
+
99.9% uptime
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
{/* Features */}
|
|
88
|
+
<section id="features" className="py-24 bg-gray-50">
|
|
89
|
+
<div className="container mx-auto px-4">
|
|
90
|
+
<div className="text-center mb-16">
|
|
91
|
+
<h2 className="text-3xl font-bold mb-4 text-gray-900">
|
|
92
|
+
Why Choose Nobalmako?
|
|
93
|
+
</h2>
|
|
94
|
+
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
|
95
|
+
Built for modern development teams who need security and professional developer experience.
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
100
|
+
<Card>
|
|
101
|
+
<CardHeader>
|
|
102
|
+
<Shield className="h-10 w-10 text-primary mb-4" />
|
|
103
|
+
<CardTitle>Enterprise Security</CardTitle>
|
|
104
|
+
<CardDescription>
|
|
105
|
+
Bank-level encryption and access controls to keep your secrets safe. SOC 2 compliant with audit trails.
|
|
106
|
+
</CardDescription>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
</Card>
|
|
109
|
+
|
|
110
|
+
<Card>
|
|
111
|
+
<CardHeader>
|
|
112
|
+
<Users className="h-10 w-10 text-primary mb-4" />
|
|
113
|
+
<CardTitle>Team Collaboration</CardTitle>
|
|
114
|
+
<CardDescription>
|
|
115
|
+
Share environment variables with your team with granular permissions. Role-based access control.
|
|
116
|
+
</CardDescription>
|
|
117
|
+
</CardHeader>
|
|
118
|
+
</Card>
|
|
119
|
+
|
|
120
|
+
<Card>
|
|
121
|
+
<CardHeader>
|
|
122
|
+
<Database className="h-10 w-10 text-primary mb-4" />
|
|
123
|
+
<CardTitle>Multi-Environment</CardTitle>
|
|
124
|
+
<CardDescription>
|
|
125
|
+
Manage variables across development, staging, and production environments with environment-specific configs.
|
|
126
|
+
</CardDescription>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
</Card>
|
|
129
|
+
|
|
130
|
+
<Card>
|
|
131
|
+
<CardHeader>
|
|
132
|
+
<Search className="h-10 w-10 text-primary mb-4" />
|
|
133
|
+
<CardTitle>Advanced Search</CardTitle>
|
|
134
|
+
<CardDescription>
|
|
135
|
+
Quickly find variables with powerful search and filtering capabilities. Search by key, value, or tags.
|
|
136
|
+
</CardDescription>
|
|
137
|
+
</CardHeader>
|
|
138
|
+
</Card>
|
|
139
|
+
|
|
140
|
+
<Card>
|
|
141
|
+
<CardHeader>
|
|
142
|
+
<Download className="h-10 w-10 text-primary mb-4" />
|
|
143
|
+
<CardTitle>Export & Import</CardTitle>
|
|
144
|
+
<CardDescription>
|
|
145
|
+
Export variables in multiple formats (JSON, .env, YAML) and import from existing configuration files.
|
|
146
|
+
</CardDescription>
|
|
147
|
+
</CardHeader>
|
|
148
|
+
</Card>
|
|
149
|
+
|
|
150
|
+
<Card>
|
|
151
|
+
<CardHeader>
|
|
152
|
+
<Upload className="h-10 w-10 text-primary mb-4" />
|
|
153
|
+
<CardTitle>Bulk Operations</CardTitle>
|
|
154
|
+
<CardDescription>
|
|
155
|
+
Update multiple variables at once with bulk edit and import features. Streamline your workflow.
|
|
156
|
+
</CardDescription>
|
|
157
|
+
</CardHeader>
|
|
158
|
+
</Card>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</section>
|
|
162
|
+
|
|
163
|
+
{/* CTA Section */}
|
|
164
|
+
<section className="py-24 bg-primary text-primary-foreground">
|
|
165
|
+
<div className="container mx-auto px-4 text-center">
|
|
166
|
+
<div className="max-w-3xl mx-auto">
|
|
167
|
+
<h2 className="text-3xl font-bold mb-6">
|
|
168
|
+
Ready to secure your environment variables?
|
|
169
|
+
</h2>
|
|
170
|
+
<p className="text-xl opacity-90 mb-8">
|
|
171
|
+
Join developers who trust Nobalmako with their sensitive data.
|
|
172
|
+
Open-source and free to use.
|
|
173
|
+
</p>
|
|
174
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
|
175
|
+
<Button size="lg" variant="secondary" asChild>
|
|
176
|
+
<Link href="/auth/register">
|
|
177
|
+
Get Started for Free
|
|
178
|
+
<ArrowRight className="ml-2 h-5 w-5" />
|
|
179
|
+
</Link>
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</section>
|
|
185
|
+
|
|
186
|
+
{/* Footer */}
|
|
187
|
+
<footer className="bg-muted py-12">
|
|
188
|
+
<div className="container mx-auto px-4">
|
|
189
|
+
<div className="grid md:grid-cols-4 gap-8 mb-8">
|
|
190
|
+
<div>
|
|
191
|
+
<div className="flex items-center space-x-2 mb-4">
|
|
192
|
+
<Shield className="h-6 w-6 text-primary" />
|
|
193
|
+
<span className="text-xl font-bold">Nobalmako</span>
|
|
194
|
+
</div>
|
|
195
|
+
<p className="text-muted-foreground text-sm">
|
|
196
|
+
Secure environment variable management for modern development teams.
|
|
197
|
+
</p>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div>
|
|
201
|
+
<h4 className="font-semibold mb-4">Product</h4>
|
|
202
|
+
<ul className="space-y-2 text-muted-foreground text-sm">
|
|
203
|
+
<li><a href="#features" className="hover:text-foreground transition-colors">Features</a></li>
|
|
204
|
+
<li><Link href="/docs#encryption" className="hover:text-foreground transition-colors">Security</Link></li>
|
|
205
|
+
</ul>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div>
|
|
209
|
+
<h4 className="font-semibold mb-4">Support</h4>
|
|
210
|
+
<ul className="space-y-2 text-muted-foreground text-sm">
|
|
211
|
+
<li><Link href="/docs" className="hover:text-foreground transition-colors">Documentation</Link></li>
|
|
212
|
+
<li><Link href="/api-reference" className="hover:text-foreground transition-colors">API Reference</Link></li>
|
|
213
|
+
</ul>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="border-t pt-8 flex flex-col md:flex-row justify-between items-center">
|
|
218
|
+
<p className="text-muted-foreground text-sm">
|
|
219
|
+
© 2026 Nobalmako. All rights reserved.
|
|
220
|
+
</p>
|
|
221
|
+
<div className="flex space-x-6 mt-4 md:mt-0">
|
|
222
|
+
<a href="#" className="text-muted-foreground hover:text-foreground transition-colors text-sm">Privacy</a>
|
|
223
|
+
<a href="#" className="text-muted-foreground hover:text-foreground transition-colors text-sm">Terms</a>
|
|
224
|
+
<a href="#" className="text-muted-foreground hover:text-foreground transition-colors text-sm">Security</a>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</footer>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useAuth } from '@/hooks/use-auth';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { Label } from '@/components/ui/label';
|
|
8
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
9
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import { User, Lock, Mail, Camera } from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
export default function ProfileClient() {
|
|
14
|
+
const { user } = useAuth();
|
|
15
|
+
const [isUpdating, setIsUpdating] = useState(false);
|
|
16
|
+
const [profileData, setProfileData] = useState({
|
|
17
|
+
name: user?.name || '',
|
|
18
|
+
email: user?.email || '',
|
|
19
|
+
});
|
|
20
|
+
const [passwordData, setPasswordData] = useState({
|
|
21
|
+
currentPassword: '',
|
|
22
|
+
newPassword: '',
|
|
23
|
+
confirmPassword: '',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const handleUpdateProfile = async (e: React.FormEvent) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
setIsUpdating(true);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch('/api/auth/update', {
|
|
32
|
+
method: 'PUT',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ name: profileData.name }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (response.ok) {
|
|
38
|
+
toast.success('Profile updated successfully');
|
|
39
|
+
// We might want to refresh the user data in the context
|
|
40
|
+
window.location.reload();
|
|
41
|
+
} else {
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
toast.error(data.error || 'Failed to update profile');
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
toast.error('An error occurred while updating profile');
|
|
47
|
+
} finally {
|
|
48
|
+
setIsUpdating(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleChangePassword = async (e: React.FormEvent) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
|
|
55
|
+
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
|
56
|
+
toast.error('New passwords do not match');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setIsUpdating(true);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch('/api/auth/update', {
|
|
64
|
+
method: 'PATCH',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
currentPassword: passwordData.currentPassword,
|
|
68
|
+
newPassword: passwordData.newPassword,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (response.ok) {
|
|
73
|
+
toast.success('Password changed successfully');
|
|
74
|
+
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
|
75
|
+
} else {
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
toast.error(data.error || 'Failed to change password');
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
toast.error('An error occurred while changing password');
|
|
81
|
+
} finally {
|
|
82
|
+
setIsUpdating(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getInitials = (userName: string) => {
|
|
87
|
+
return userName
|
|
88
|
+
.split(' ')
|
|
89
|
+
.map((n) => n[0])
|
|
90
|
+
.join('')
|
|
91
|
+
.toUpperCase();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="max-w-4xl mx-auto space-y-8">
|
|
96
|
+
<div>
|
|
97
|
+
<h1 className="text-3xl font-bold tracking-tight">Account Settings</h1>
|
|
98
|
+
<p className="text-muted-foreground">Manage your personal information and security preferences.</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
102
|
+
<div className="md:col-span-1">
|
|
103
|
+
<Card>
|
|
104
|
+
<CardHeader className="text-center">
|
|
105
|
+
<div className="flex justify-center mb-4">
|
|
106
|
+
<div className="relative group">
|
|
107
|
+
<Avatar className="h-24 w-24">
|
|
108
|
+
<AvatarImage src="" />
|
|
109
|
+
<AvatarFallback className="text-2xl font-bold">
|
|
110
|
+
{user?.name ? getInitials(user.name) : 'U'}
|
|
111
|
+
</AvatarFallback>
|
|
112
|
+
</Avatar>
|
|
113
|
+
<Button
|
|
114
|
+
size="icon"
|
|
115
|
+
variant="secondary"
|
|
116
|
+
className="absolute bottom-0 right-0 rounded-full w-8 h-8 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
117
|
+
>
|
|
118
|
+
<Camera className="h-4 w-4" />
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<CardTitle>{user?.name}</CardTitle>
|
|
123
|
+
<CardDescription>{user?.email}</CardDescription>
|
|
124
|
+
</CardHeader>
|
|
125
|
+
<CardContent>
|
|
126
|
+
<div className="bg-muted p-4 rounded-lg text-sm text-balance text-center">
|
|
127
|
+
Member since {new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
|
|
128
|
+
</div>
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="md:col-span-2 space-y-8">
|
|
134
|
+
{/* Profile Information */}
|
|
135
|
+
<Card>
|
|
136
|
+
<form onSubmit={handleUpdateProfile}>
|
|
137
|
+
<CardHeader>
|
|
138
|
+
<CardTitle className="flex items-center gap-2">
|
|
139
|
+
<User className="h-5 w-5" />
|
|
140
|
+
Profile Information
|
|
141
|
+
</CardTitle>
|
|
142
|
+
<CardDescription>Update your name and contact details.</CardDescription>
|
|
143
|
+
</CardHeader>
|
|
144
|
+
<CardContent className="space-y-4">
|
|
145
|
+
<div className="space-y-2">
|
|
146
|
+
<Label htmlFor="name">Full Name</Label>
|
|
147
|
+
<Input
|
|
148
|
+
id="name"
|
|
149
|
+
value={profileData.name}
|
|
150
|
+
onChange={(e) => setProfileData({ ...profileData, name: e.target.value })}
|
|
151
|
+
required
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="space-y-2">
|
|
155
|
+
<Label htmlFor="email">Email Address</Label>
|
|
156
|
+
<Input
|
|
157
|
+
id="email"
|
|
158
|
+
type="email"
|
|
159
|
+
value={profileData.email}
|
|
160
|
+
disabled
|
|
161
|
+
className="bg-muted"
|
|
162
|
+
/>
|
|
163
|
+
<p className="text-xs text-muted-foreground">Email address cannot be changed at this time.</p>
|
|
164
|
+
</div>
|
|
165
|
+
</CardContent>
|
|
166
|
+
<CardFooter className="flex justify-end bg-muted/50 py-3">
|
|
167
|
+
<Button type="submit" disabled={isUpdating || profileData.name === user?.name}>
|
|
168
|
+
{isUpdating ? 'Saving...' : 'Save Changes'}
|
|
169
|
+
</Button>
|
|
170
|
+
</CardFooter>
|
|
171
|
+
</form>
|
|
172
|
+
</Card>
|
|
173
|
+
|
|
174
|
+
{/* Change Password */}
|
|
175
|
+
<Card>
|
|
176
|
+
<form onSubmit={handleChangePassword}>
|
|
177
|
+
<CardHeader>
|
|
178
|
+
<CardTitle className="flex items-center gap-2">
|
|
179
|
+
<Lock className="h-5 w-5" />
|
|
180
|
+
Security
|
|
181
|
+
</CardTitle>
|
|
182
|
+
<CardDescription>Secure your account by updating your password.</CardDescription>
|
|
183
|
+
</CardHeader>
|
|
184
|
+
<CardContent className="space-y-4">
|
|
185
|
+
<div className="space-y-2">
|
|
186
|
+
<Label htmlFor="current-password">Current Password</Label>
|
|
187
|
+
<Input
|
|
188
|
+
id="current-password"
|
|
189
|
+
type="password"
|
|
190
|
+
value={passwordData.currentPassword}
|
|
191
|
+
onChange={(e) => setPasswordData({ ...passwordData, currentPassword: e.target.value })}
|
|
192
|
+
required
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
196
|
+
<div className="space-y-2">
|
|
197
|
+
<Label htmlFor="new-password">New Password</Label>
|
|
198
|
+
<Input
|
|
199
|
+
id="new-password"
|
|
200
|
+
type="password"
|
|
201
|
+
value={passwordData.newPassword}
|
|
202
|
+
onChange={(e) => setPasswordData({ ...passwordData, newPassword: e.target.value })}
|
|
203
|
+
required
|
|
204
|
+
minLength={8}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="space-y-2">
|
|
208
|
+
<Label htmlFor="confirm-password">Confirm New Password</Label>
|
|
209
|
+
<Input
|
|
210
|
+
id="confirm-password"
|
|
211
|
+
type="password"
|
|
212
|
+
value={passwordData.confirmPassword}
|
|
213
|
+
onChange={(e) => setPasswordData({ ...passwordData, confirmPassword: e.target.value })}
|
|
214
|
+
required
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</CardContent>
|
|
219
|
+
<CardFooter className="flex justify-end bg-muted/50 py-3">
|
|
220
|
+
<Button type="submit" disabled={isUpdating || !passwordData.currentPassword || !passwordData.newPassword}>
|
|
221
|
+
{isUpdating ? 'Updating...' : 'Change Password'}
|
|
222
|
+
</Button>
|
|
223
|
+
</CardFooter>
|
|
224
|
+
</form>
|
|
225
|
+
</Card>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|