Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,35 @@ import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";

const nextConfig: NextConfig = {
/* config options here */
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none';",
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Strict-Transport-Security",
value: "max-age=31536000; includeSubDomains",
},
],
},
];
},
};

const sentryConfig = {
Expand Down
31 changes: 31 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://smshub.dev/</loc>
<priority>1.0</priority>
</url>
<url>
<loc>https://smshub.dev/login</loc>
<priority>0.8</priority>
</url>
<url>
<loc>https://smshub.dev/register</loc>
<priority>0.8</priority>
</url>
<url>
<loc>https://smshub.dev/install</loc>
<priority>0.7</priority>
</url>
<url>
<loc>https://smshub.dev/phonenumbers</loc>
<priority>0.7</priority>
</url>
<url>
<loc>https://smshub.dev/privacy</loc>
<priority>0.3</priority>
</url>
<url>
<loc>https://smshub.dev/terms</loc>
<priority>0.3</priority>
</url>
</urlset>
12 changes: 12 additions & 0 deletions src/app/api/__tests__/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, it, expect } from "vitest";

describe("GET /api/health", () => {
it("returns 200 with status ok", async () => {
const { GET } = await import("@/app/api/health/route");
const response = await GET();
expect(response.status).toBe(200);
const json = await response.json();
expect(json.status).toBe("ok");
expect(json.timestamp).toBeDefined();
});
});
91 changes: 91 additions & 0 deletions src/app/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";

import { useState } from "react";
import { createClient } from "@/lib/supabase/client";
import Link from "next/link";
import { Logo } from "@/components/logo";

export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);

const handleReset = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);

const supabase = createClient();
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password`,
});

if (error) {
setError(error.message);
} else {
setSuccess(true);
}
setLoading(false);
};

return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-sm space-y-6">
<div className="text-center">
<div className="flex justify-center">
<Logo
imageClassName="h-20 w-auto"
className="flex items-center gap-3"
priority
/>
</div>
<p className="text-gray-400 mt-2">Reset your password</p>
</div>

{success ? (
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 text-green-400 text-sm text-center">
Check your email for a password reset link.
</div>
) : (
<form onSubmit={handleReset} className="space-y-4">
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 text-red-400 text-sm">
{error}
</div>
)}

<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-1">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="you@example.com"
/>
</div>

<button
type="submit"
disabled={loading}
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-lg font-medium transition-colors"
>
{loading ? "Sending..." : "Send Reset Link"}
</button>
</form>
)}

<p className="text-center text-sm text-gray-400">
<Link href="/login" className="text-blue-400 hover:text-blue-300">
Back to sign in
</Link>
</p>
</div>
</div>
);
}
10 changes: 9 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export const metadata: Metadata = {
title: "SMSHub",
},
other: {
"mobile-web-app-capable": "yes",
"msapplication-TileColor": "#030712",
"msapplication-config": "/browserconfig.xml",
"msapplication-TileImage": "/icons/apple-touch-icon-144x144.png",
Expand All @@ -51,12 +50,21 @@ export const metadata: Metadata = {
url: "https://smshub.dev",
siteName: "SMSHub",
type: "website",
images: [
{
url: "/icons/icon-512x512.png",
width: 512,
height: 512,
alt: "SMSHub",
},
],
},
twitter: {
card: "summary_large_image",
title: "SMSHub — Multi-platform SMS Messaging",
description:
"One inbox for all your SMS. Web, iOS, Android, desktop. Multi-provider support with developer-first API.",
images: ["/icons/icon-512x512.png"],
},
};

Expand Down
6 changes: 6 additions & 0 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,15 @@ export default function LoginPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="••••••••"
/>
<div className="mt-1 text-right">
<Link href="/forgot-password" className="text-xs text-blue-400 hover:text-blue-300">
Forgot password?
</Link>
</div>
</div>

<button
Expand Down
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export default function LandingPage() {
"Priority support",
]}
cta="Contact Sales"
href="/register"
href="mailto:sales@smshub.dev"
/>
</div>
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/app/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export default function RegisterPage() {
return;
}

if (password.length < 6) {
setError("Password must be at least 6 characters");
if (password.length < 8) {
setError("Password must be at least 8 characters");
setLoading(false);
return;
}
Expand Down Expand Up @@ -92,6 +92,9 @@ export default function RegisterPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
pattern=".{8,}"
title="Password must be at least 8 characters"
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="••••••••"
/>
Expand All @@ -107,6 +110,7 @@ export default function RegisterPage() {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="••••••••"
/>
Expand Down
80 changes: 63 additions & 17 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
"use client";

import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Logo } from "@/components/logo";

const APP_ROUTES = ["/inbox", "/settings", "/campaigns", "/contacts", "/analytics", "/org"];

const NAV_LINKS = [
{ href: "/#features", label: "Features" },
{ href: "/#pricing", label: "Pricing" },
{ href: "/#api", label: "API" },
{ href: "/#apps", label: "Apps" },
{ href: "/phonenumbers", label: "Phone Numbers" },
];

export function Header() {
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

// Hide header on app routes (inbox has its own nav)
if (APP_ROUTES.some((r) => pathname.startsWith(r))) return null;
Expand All @@ -18,38 +28,74 @@ export function Header() {
<Logo priority />

<nav className="hidden sm:flex items-center gap-6">
<Link href="/#features" className="text-sm text-gray-400 hover:text-white transition-colors">
Features
</Link>
<Link href="/#pricing" className="text-sm text-gray-400 hover:text-white transition-colors">
Pricing
</Link>
<Link href="/#api" className="text-sm text-gray-400 hover:text-white transition-colors">
API
</Link>
<Link href="/#apps" className="text-sm text-gray-400 hover:text-white transition-colors">
Apps
</Link>
<Link href="/phonenumbers" className="text-sm text-gray-400 hover:text-white transition-colors">
Phone Numbers
</Link>
{NAV_LINKS.map((link) => (
<Link key={link.href} href={link.href} className="text-sm text-gray-400 hover:text-white transition-colors">
{link.label}
</Link>
))}
</nav>

<div className="flex items-center gap-3">
<Link
href="/login"
className="text-sm text-gray-300 hover:text-white transition-colors"
className="text-sm text-gray-300 hover:text-white transition-colors hidden sm:inline"
>
Sign in
</Link>
<Link
href="/install"
className="text-sm px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition-colors"
className="text-sm px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition-colors hidden sm:inline-block"
>
Get Started
</Link>

{/* Mobile hamburger */}
<button
className="sm:hidden p-2 text-gray-400 hover:text-white transition-colors"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
</div>

{/* Mobile dropdown */}
{mobileMenuOpen && (
<nav className="sm:hidden border-t border-gray-800 bg-gray-950 px-4 py-4 space-y-3">
{NAV_LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
className="block text-sm text-gray-400 hover:text-white transition-colors"
onClick={() => setMobileMenuOpen(false)}
>
{link.label}
</Link>
))}
<hr className="border-gray-800" />
<Link
href="/login"
className="block text-sm text-gray-300 hover:text-white transition-colors"
onClick={() => setMobileMenuOpen(false)}
>
Sign in
</Link>
<Link
href="/install"
className="block text-sm text-center px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition-colors"
onClick={() => setMobileMenuOpen(false)}
>
Get Started
</Link>
</nav>
)}
</header>
);
}
Loading
Loading