feat(auth,explore): Add NSFW content gating and age verification

- Add automatic NSFW settings for users registering on NSFW nodes
- Implement age verification checkbox on registration form for NSFW nodes
- Add NSFW node detection on explore page with gated content access
- Restrict unauthenticated users from viewing posts on NSFW nodes
- Fetch and display node NSFW status in explore and login pages
- Update node info type to include isNsfw flag across auth flows
- Display age-gated content warning with EyeOff icon for restricted access
- Enforce age verification requirement before registration on adult nodes
This commit is contained in:
AskIt
2026-01-26 01:54:02 +01:00
parent 091c85080b
commit 981441bfe5
4 changed files with 78 additions and 9 deletions
+18
View File
@@ -1,5 +1,7 @@
import { NextResponse } from 'next/server';
import { registerUser, createSession } from '@/lib/auth';
import { db, nodes, users } from '@/db';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
const registerSchema = z.object({
@@ -21,6 +23,22 @@ export async function POST(request: Request) {
data.displayName
);
// Check if this is an NSFW node and auto-enable NSFW settings
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const node = await db.query.nodes.findFirst({
where: eq(nodes.domain, nodeDomain),
});
if (node?.isNsfw) {
// Auto-enable NSFW viewing and mark account as NSFW for users on NSFW nodes
await db.update(users)
.set({
nsfwEnabled: true,
isNsfw: true
})
.where(eq(users.id, user.id));
}
// Create session for new user
await createSession(user.id);
+25 -2
View File
@@ -6,7 +6,8 @@ import { SearchIcon, TrendingIcon, UsersIcon } from '@/components/Icons';
import { PostCard } from '@/components/PostCard';
import { Post } from '@/lib/types';
import { formatFullHandle } from '@/lib/utils/handle';
import { Bot, Network, Server } from 'lucide-react';
import { Bot, Network, Server, EyeOff } from 'lucide-react';
import { useAuth } from '@/lib/contexts/AuthContext';
interface User {
id: string;
@@ -75,6 +76,7 @@ interface SwarmPost {
}
export default function ExplorePage() {
const { user } = useAuth();
const [query, setQuery] = useState('');
const [activeTab, setActiveTab] = useState<'node' | 'swarm' | 'users' | 'search'>('node');
const [nodePosts, setNodePosts] = useState<Post[]>([]);
@@ -84,6 +86,17 @@ export default function ExplorePage() {
const [searchResults, setSearchResults] = useState<{ posts: Post[]; users: User[] }>({ posts: [], users: [] });
const [loading, setLoading] = useState(true);
const [searching, setSearching] = useState(false);
const [isNsfwNode, setIsNsfwNode] = useState(false);
// Fetch node info to check if NSFW
useEffect(() => {
fetch('/api/node')
.then(res => res.json())
.then(data => {
setIsNsfwNode(data.isNsfw || false);
})
.catch(() => {});
}, []);
useEffect(() => {
// Load node posts (local only)
@@ -232,7 +245,17 @@ export default function ExplorePage() {
<div className="explore-content">
{activeTab === 'node' && (
loading ? (
!user && isNsfwNode ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<EyeOff size={48} style={{ marginBottom: '16px', opacity: 0.5 }} />
<p style={{ fontSize: '16px', fontWeight: 500, color: 'var(--foreground-secondary)', marginBottom: '8px' }}>
Adult Content
</p>
<p style={{ fontSize: '14px', maxWidth: '320px', margin: '0 auto' }}>
This node contains adult or sensitive content. You must be 18 or older and signed in to view posts.
</p>
</div>
) : loading ? (
<div className="explore-loading">Loading posts...</div>
) : nodePosts.length === 0 ? (
<div className="explore-empty">
+33 -2
View File
@@ -15,8 +15,9 @@ export default function LoginPage() {
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [nodeInfoLoaded, setNodeInfoLoaded] = useState(false);
const [nodeInfo, setNodeInfo] = useState<{ name: string; description: string; logoUrl?: string }>({ name: '', description: '' });
const [nodeInfo, setNodeInfo] = useState<{ name: string; description: string; logoUrl?: string; isNsfw?: boolean }>({ name: '', description: '' });
const [handleStatus, setHandleStatus] = useState<'idle' | 'checking' | 'available' | 'taken'>('idle');
const [ageVerified, setAgeVerified] = useState(false);
// Import specific state
const [importFile, setImportFile] = useState<File | null>(null);
@@ -33,7 +34,8 @@ export default function LoginPage() {
setNodeInfo({
name: data.name || '',
description: data.description || 'Synapsis is designed to function like a global signal layer rather than a culture-bound platform. Anyone can run their own node and still participate in a shared, interconnected network, with global identity, clean terminology, and a modern interface that feels current rather than experimental. Synapsis aims to be neutral, resilient infrastructure for human and machine discourse, more like a protocol or nervous system than a social club.',
logoUrl: data.logoUrl || undefined
logoUrl: data.logoUrl || undefined,
isNsfw: data.isNsfw || false
});
setNodeInfoLoaded(true);
})
@@ -122,6 +124,11 @@ export default function LoginPage() {
return;
}
if (mode === 'register' && nodeInfo.isNsfw && !ageVerified) {
setError('You must verify your age to register on this node');
return;
}
setLoading(true);
try {
@@ -380,6 +387,30 @@ export default function LoginPage() {
</div>
)}
{mode === 'register' && nodeInfo.isNsfw && (
<div style={{
marginBottom: '20px',
padding: '12px',
background: 'rgba(239, 68, 68, 0.05)',
border: '1px solid rgba(239, 68, 68, 0.2)',
borderRadius: 'var(--radius-md)',
}}>
<label style={{ display: 'flex', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={ageVerified}
onChange={(e) => setAgeVerified(e.target.checked)}
style={{ marginTop: '3px' }}
/>
<span style={{ fontSize: '12px', color: 'var(--foreground-secondary)', lineHeight: 1.4 }}>
<strong style={{ color: 'var(--error)', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<TriangleAlert size={12} /> Age Verification:
</strong> This node contains adult or sensitive content. I confirm that I am at least 18 years of age.
</span>
</label>
</div>
)}
<button
type="submit"
className="btn btn-primary btn-lg"
+2 -5
View File
@@ -159,17 +159,14 @@ export default function Home() {
{/* NSFW node gate for unauthenticated users */}
{!user && isNsfwNode ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<EyeOff size={48} style={{ marginBottom: '16px', opacity: 0.5 }} />
<p style={{ fontSize: '16px', fontWeight: 500, color: 'var(--foreground-secondary)', marginBottom: '8px' }}>
Adult Content
</p>
<p style={{ fontSize: '14px', maxWidth: '320px', margin: '0 auto 20px' }}>
<p style={{ fontSize: '14px', maxWidth: '320px', margin: '0 auto' }}>
This node contains adult or sensitive content. You must be 18 or older and signed in to view posts.
</p>
<Link href="/login" className="btn btn-primary">
Sign In to Continue
</Link>
</div>
) : loading ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>