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
41 changes: 41 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
# Dependencies
node_modules
.pnp
.pnp.js

# Testing
coverage
*.log

# Next.js
.next/
out/
build
dist

# Production
.vercel
.env*.local

# Environment files
.env
.env.production

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# OS
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# Misc
.turbo
.cache
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,27 @@ This project is deployed on Vercel. Check out the live version at [basednet.verc

## Features

### Core Features ✅
- **Personal Websites**: Create and customize your own personal website with HTML, CSS, and JavaScript
- **IPFS Integration**: Host your content on the decentralized IPFS network
- **Webrings**: Join and create webrings to connect with like-minded creators
- Create your own webrings
- Join existing communities
- Navigate through webring members (next, previous, random)
- **Windows 98 Aesthetic**: Enjoy a nostalgic user interface inspired by Windows 98
- **User Profiles**: Customize your profile with bio, avatar, and social links
- **Content Management**: Upload, manage, and pin your IPFS content
- **User Discovery**: Browse and search for creators and their sites
- **Input Validation**: All API endpoints protected with Zod schema validation
- **Security**: Rate limiting, security headers, and input sanitization

### Pages
- **Home** (`/`): Platform overview and statistics
- **Browse** (`/browse`): Discover and search for user sites
- **Webrings** (`/webrings`): View, create, join, and manage webrings
- **Dashboard** (`/dashboard`): Manage your IPFS content
- **Profile** (`/profile`): Edit your profile and customize your site
- **Help** (`/help`): Comprehensive documentation and FAQ

## Getting Started

Expand Down
71 changes: 71 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,

// Performance optimizations
compress: true,
poweredByHeader: false,

// Image optimization
images: {
domains: ['ipfs.io', 'gateway.pinata.cloud', 'cloudflare-ipfs.com'],
formats: ['image/avif', 'image/webp'],
},
Comment on lines +12 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Security Vulnerability: The domains configuration is deprecated and creates security risks. Use remotePatterns instead to properly validate image sources and prevent potential SSRF attacks.

Suggested change
domains: ['ipfs.io', 'gateway.pinata.cloud', 'cloudflare-ipfs.com'],
formats: ['image/avif', 'image/webp'],
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'ipfs.io',
pathname: '/ipfs/**',
},
{
protocol: 'https',
hostname: 'gateway.pinata.cloud',
pathname: '/ipfs/**',
},
{
protocol: 'https',
hostname: 'cloudflare-ipfs.com',
pathname: '/ipfs/**',
},
],
formats: ['image/avif', 'image/webp'],
},


// Security headers
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
Comment on lines +43 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Permissions-Policy header is set to disable all permissions for camera, microphone, and geolocation. This is a secure default, but if your application needs to use any of these features, this setting will prevent their use. Consider adjusting the policy to enable specific permissions as needed.

For example, if your application needs camera access:

value: 'camera=(self), microphone=(), geolocation=()'

}
],
},
]
},

// Webpack configuration
webpack: (config, { isServer }) => {
// Handle node modules that might need special treatment
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
}
Comment on lines +55 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webpack configuration disables certain node modules (fs, net, tls) when not on the server. This is typically done to prevent errors in environments that do not support these modules. However, ensure that this does not affect the functionality of your application or expose it to vulnerabilities. If these modules are required by any client-side code, this configuration could lead to runtime errors or broken functionality.

}
return config
},

// Environment variables that should be available client-side
env: {
NEXT_PUBLIC_IPFS_GATEWAY: process.env.IPFS_GATEWAY || 'https://ipfs.io/ipfs/',
},
}

module.exports = nextConfig
103 changes: 1 addition & 102 deletions src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,5 @@
import { NextAuthOptions } from 'next-auth';
import NextAuth from 'next-auth/next';
import { withTransaction } from '@/lib/db';

export const authOptions: NextAuthOptions = {
providers: [
{
id: 'indieauth',
name: 'IndieAuth',
type: 'oauth',
authorization: {
url: 'https://indieauth.com/auth',
params: { scope: 'profile email' }
},
token: {
url: 'https://tokens.indieauth.com/token',
},
userinfo: {
url: 'https://indieauth.com/userinfo',
async request({ tokens, client }) {
// IndieAuth specific user info handling
return {
id: tokens.me,
name: tokens.name || tokens.me,
email: tokens.email,
image: tokens.photo,
};
},
},
profile(profile) {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.image,
};
},
clientId: process.env.INDIE_AUTH_CLIENT_ID,
clientSecret: process.env.INDIE_AUTH_CLIENT_SECRET,
},
],
callbacks: {
async signIn({ user, account, profile }) {
try {
await withTransaction(async (client) => {
// Check if user exists
const result = await client.query(
'SELECT * FROM users WHERE auth_domain = $1',
[profile.id]
);

if (result.rows.length === 0) {
// Create new user
await client.query(
'INSERT INTO users (username, auth_domain, email) VALUES ($1, $2, $3)',
[profile.name, profile.id, profile.email]
);

// Create empty profile
await client.query(
'INSERT INTO profiles (user_id) VALUES (currval(\'users_id_seq\'))'
);
}
});
return true;
} catch (error) {
console.error('Error during sign in:', error);
return false;
}
},
async session({ session, user }) {
try {
const result = await withTransaction(async (client) => {
const userResult = await client.query(
'SELECT * FROM users WHERE email = $1',
[session.user?.email]
);

if (userResult.rows[0]) {
return {
...session,
user: {
...session.user,
id: userResult.rows[0].id,
username: userResult.rows[0].username,
},
};
}
return session;
});
return result;
} catch (error) {
console.error('Error getting session:', error);
return session;
}
},
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
},
debug: process.env.NODE_ENV === 'development',
};
import { authOptions } from '@/lib/authOptions';

const handler = NextAuth(authOptions);

Expand Down
14 changes: 9 additions & 5 deletions src/app/api/ipfs/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '../../../lib/auth';
import { IpfsContentModel } from '../../../db/models/ipfs-content';
import { createIpfsContentSchema, validateBody } from '../../../lib/validation';

export async function GET(req: NextRequest) {
try {
Expand Down Expand Up @@ -33,20 +34,23 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const data = await req.json();
const { cid, contentType, filename, size } = data;
const body = await req.json();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code does not handle potential exceptions that might be thrown if req.json() fails due to malformed JSON input. This can lead to unhandled exceptions or incorrect error responses. Suggestion: Wrap the req.json() call in a try-catch block and return a meaningful error response if an exception occurs. This will prevent the server from crashing and provide a clearer error message to the client.


if (!cid) {
// Validate input
const validation = validateBody(createIpfsContentSchema, body);
if (!validation.success) {
return NextResponse.json(
Comment on lines +41 to 42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling for validation failures could be improved by providing more specific error messages. Currently, the error message returned is generic and derived from the validation function. Suggestion: Modify the validateBody function to include detailed error messages for each type of validation failure. This will help clients understand what part of their request was invalid, leading to a more user-friendly API.

Comment on lines +41 to 42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in the POST method could be improved by providing more detailed error messages. Currently, it only returns a generic error message from the validation process. Suggestion: Enhance the error response by including specific details about what part of the input validation failed. This can help the client understand what went wrong and how to fix it.

Example:

if (!validation.success) {
  return NextResponse.json(
    { error: `Validation failed: ${validation.error}` },
    { status: 400 }
  );
}

{ error: 'CID is required' },
{ error: validation.error },
{ status: 400 }
);
}

const { cid, content_type, filename, size } = validation.data;

const content = await IpfsContentModel.create(
user.id,
cid,
contentType,
content_type,
filename,
size
Comment on lines 50 to 55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The creation of IPFS content involves multiple database operations which could potentially slow down the response time, especially under high load. Suggestion: Consider optimizing these operations by using batch inserts or transactions if multiple records are being inserted simultaneously. Additionally, ensure that the database operations are properly indexed to speed up the queries.

Example:

const content = await IpfsContentModel.create(
  user.id,
  cid,
  content_type,
  filename,
  size
);

);
Expand Down
17 changes: 14 additions & 3 deletions src/app/api/profile/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser, requireAuth } from '../../../lib/auth';
import { ProfileModel } from '../../../db/models/profile';
import { updateProfileSchema, validateBody } from '../../../lib/validation';

export async function GET(req: NextRequest) {
try {
Expand All @@ -27,9 +28,19 @@ export async function PUT(req: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const data = await req.json();
const profile = await ProfileModel.update(user.id, data);

const body = await req.json();

// Validate input
const validation = validateBody(updateProfileSchema, body);
if (!validation.success) {
return NextResponse.json(
{ error: validation.error },
{ status: 400 }
Comment on lines +36 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message returned when the profile update validation fails is generic and may not provide sufficient information for debugging or user understanding. Consider enhancing the error details by including specific validation failures in the response. This can be achieved by modifying the error object to include more context about what exactly failed during validation.

Suggested Change:

return NextResponse.json({ error: 'Validation failed', details: validation.errors }, { status: 400 });

);
}

const profile = await ProfileModel.update(user.id, validation.data);

if (!profile) {
return NextResponse.json(
{ error: 'Profile not found' },
Comment on lines 44 to 46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message 'Profile not found' in the response could be misleading in the context of a profile update operation. It suggests that the profile does not exist, whereas the actual issue might be related to the update operation failing (e.g., due to validation issues or database constraints).

Recommendation: Consider providing a more specific error message that clarifies the nature of the error, such as 'Failed to update profile' or 'Profile update not successful'. This will improve the clarity of the API responses and help client-side developers handle errors more effectively.

Comment on lines +42 to 46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The update operation assumes the profile does not exist if no records are affected, which might not always be the case. This could be misleading if the update fails due to other reasons, such as database connectivity issues. It's recommended to check for the existence of the profile before attempting an update and handle different error scenarios more explicitly.

Suggested Change:
Before performing the update, check if the profile exists and handle different failure scenarios distinctly to provide more accurate feedback and avoid unnecessary operations.

const existingProfile = await ProfileModel.findByUserId(user.id);
if (!existingProfile) {
  return NextResponse.json({ error: 'Profile not found' }, { status: 404 });
}
// Proceed with update

Expand Down
Loading