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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)' }}>
|
||||
|
||||
Reference in New Issue
Block a user