A modern and scalable system. Developed with Next.js 15, TypeScript, Knex.js, and Ant Design.
- Modern and responsive user interface (Ant Design v5)
- Advanced authentication and authorization system
- Multi-organization support
- Detailed permission and role management
- Dark/Light theme support
- Real-time data synchronization
- Mobile-responsive design
The system keeps track of all authentication attempts, including:
- Login attempts (successful/failed)
- IP addresses
- Browser information
- Login timestamps
- Framework: Next.js 15 (App Router)
- Language: TypeScript
- Database: PostgreSQL
- Query Builder: Knex.js
- UI: Ant Design
- Authentication: NextAuth.js
nextstarter/
├── app/ # Next.js App Router structure
│ ├── (protected)/ # Protected routes
│ │ ├── dashboard/ # Dashboard and statistics
│ │ │ ├── page.tsx # Main dashboard page
│ │ │ └── components/ # Dashboard components
│ │ └── administrations/ # Administration panel
│ │ ├── users/ # User management
│ │ │ ├── page.tsx # User list
│ │ │ └── profile/ # Profile management
│ │ ├── organizations/ # Organization management
│ │ ├── roles/ # Role management
│ │ └── permissions/ # Permission management
│ ├── api/ # API endpoints
│ │ ├── auth/ # Authentication APIs
│ │ ├── dashboard/ # Dashboard APIs
│ │ │ ├── stats/ # Statistics APIs
│ │ │ └── activity/ # Activity APIs
│ │ └── administrations/ # Administration APIs
│ │ ├── users/ # User APIs
│ │ ├── organizations/ # Organization APIs
│ │ ├── roles/ # Role APIs
│ │ ├── permissions/ # Permission APIs
│ │ ├── resources/ # Resource APIs
│ │ └── actions/ # Action APIs
│ └── auth/ # Authentication pages
├── contexts/ # React Contexts
│ └── NotificationContext.tsx # Notification management
├── lib/ # Helper functions and services
│ ├── apiClient/ # API client
│ │ └── index.ts # Central API operations
│ ├── auth/ # Authentication and authorization
│ │ ├── auth-options.ts # NextAuth configuration
│ │ ├── permissions.ts # Permission control hooks
│ │ └── session.ts # Session management
│ └── utils/ # Utility functions
├── hooks/ # Custom React hooks
│ └── useNotificationSetup.ts # Notification hook
└── knex/ # Database configuration
├── knexfile.ts # Knex configuration
├── index.ts # Knex database client
├── setdb.ts # Database schema creation
├── seed.ts # Database seeding
├── adapters/ # Database adapters
│ └── nextauth-knex-adapter.ts # NextAuth Knex adapter
├── types/ # TypeScript type definitions
│ ├── user.ts # User types
│ ├── organization.ts # Organization types
│ ├── role.ts # Role types
│ └── permission.ts # Permission types
└── migrations/ # Database migrations
Represents protected entities in the system.
// Example resource definitions
const resources = [
{ name: 'Product', slug: 'product' },
{ name: 'Customer', slug: 'customer' },
{ name: 'Order', slug: 'order' },
];
Defines operations that can be performed on resources.
// Example action definitions
const actions = [
{ name: 'View', slug: 'view' },
{ name: 'Create', slug: 'create' },
{ name: 'Edit', slug: 'edit' },
{ name: 'Delete', slug: 'delete' },
];
Assigning permission directly to a user:
// Example: Give John permission to view and edit products
const permission = {
target: 'USER',
userId: 'john_123',
resourceId: 'product',
actions: ['view', 'edit'],
};
Assigning permission to a role:
// Example: Give Sales Manager role customer and order management permissions
const permission = {
target: 'ROLE',
roleId: 'sales_manager',
resourceId: 'customer',
actions: ['view', 'create', 'edit', 'delete'],
};
Assigning permission to an entire organization:
// Example: Give Branch X permission to view orders
const permission = {
target: 'ORGANIZATION',
organizationId: 'branch_x',
resourceId: 'order',
actions: ['view'],
};
// Permission check within component
const CanEditProduct = () => {
const hasPermission = usePermission("product", "edit");
if (!hasPermission) {
return <div>You don't have permission for this operation</div>;
}
return <EditProductForm />;
};
// Permission check in API route
export async function PUT(request: NextRequest) {
try {
await requirePermission('product', 'edit');
// Continue if permission exists
} catch (error) {
return new NextResponse('Unauthorized', { status: 401 });
}
}
GET /api/administrations/permissions
: List all permissionsPOST /api/administrations/permissions
: Create new permissionPUT /api/administrations/permissions/[id]
: Update permissionDELETE /api/administrations/permissions/[id]
: Delete permission
GET /api/administrations/resources
: List all resourcesPOST /api/administrations/resources
: Create new resourcePUT /api/administrations/resources/[id]
: Update resourceDELETE /api/administrations/resources/[id]
: Delete resource
GET /api/administrations/actions
: List all actionsPOST /api/administrations/actions
: Create new actionPUT /api/administrations/actions/[id]
: Update actionDELETE /api/administrations/actions/[id]
: Delete action
// 1. Create Sales Role
const salesRole = await knex('roles')
.insert({
name: 'Sales Team',
description: 'Sales team members',
})
.returning('*')
.first();
// 2. Assign Permissions to Sales Role
const permissions = [
{
target: 'ROLE',
roleId: salesRole.id,
resourceId: 'customer',
actions: ['view', 'create', 'edit'],
},
{
target: 'ROLE',
roleId: salesRole.id,
resourceId: 'order',
actions: ['view', 'create'],
},
];
// 3. Apply Permissions
await Promise.all(permissions.map((perm) => knex('permissions').insert(perm)));
// 1. Create Regional Manager Role
const managerRole = await knex('roles')
.insert({
name: 'Regional Manager',
description: 'Regional management team',
})
.returning('*')
.first();
// 2. Create Region Organization
const regionOrg = await knex('organizations')
.insert({
name: 'East Region',
code: 'EAST_001',
})
.returning('*')
.first();
// 3. Assign Organization-wide Permissions
const permissions = [
{
target: 'ORGANIZATION',
organizationId: regionOrg.id,
resourceId: 'sales_report',
actions: ['view'],
},
{
target: 'ORGANIZATION',
organizationId: regionOrg.id,
resourceId: 'performance_metrics',
actions: ['view', 'edit'],
},
];
await Promise.all(permissions.map((perm) => knex('permissions').insert(perm)));
The notification system uses Ant Design's notification component with a custom context setup:
// types.ts
export type NotificationType = 'success' | 'error' | 'info' | 'warning';
export interface ShowNotificationFunction {
(type: NotificationType, message: string, description?: string): void;
}
// NotificationContext.tsx
const NotificationContext = createContext<NotificationContextType | null>(null);
export const NotificationProvider = ({ children }: { children: React.ReactNode }) => {
const [notificationApi, contextHolder] = notification.useNotification();
const showNotification: ShowNotificationFunction = (type, message, description) => {
notificationApi[type]({
message,
description,
placement: 'topRight'
});
};
return (
<NotificationContext.Provider value={{ showNotification, notificationApi }}>
{contextHolder}
{children}
</NotificationContext.Provider>
);
};
export const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotification must be used within a NotificationProvider');
}
return context;
};
Usage in API Client:
// Success case (POST/PUT/DELETE)
if (['POST', 'PUT', 'DELETE'].includes(response.config.method?.toUpperCase() || '')) {
const message =
response.config.method?.toUpperCase() === 'DELETE'
? 'Deletion successful!'
: 'Process completed successfully';
const showNotification = window.__showNotification as ShowNotificationFunction;
showNotification?.('success', message);
}
// Error case
const errorMessage = (error.response?.data as string) || error.message;
const showNotification = window.__showNotification as ShowNotificationFunction;
const truncatedMessage =
errorMessage.length > 500 ? errorMessage.slice(0, 497) + '...' : errorMessage;
showNotification?.('error', 'Hata', truncatedMessage);
-
Consistent Usage
- Use the same notification system across the entire application
- Maintain consistent message formats and durations
-
Error Handling
- Always provide clear error messages
- Include relevant error details in the description
- Add action buttons for error recovery when applicable
-
User Experience
- Keep notifications concise and informative
- Use appropriate notification types
- Don't overwhelm users with too many notifications
- Consider notification stacking and positioning
-
Integration with API Calls
const apiCall = async () => {
try {
const response = await fetch('/api/data');
const data = await response.json();
showNotification({
type: 'success',
message: 'Data Retrieved',
description: 'Successfully fetched the requested data.',
});
return data;
} catch (error) {
showNotification({
type: 'error',
message: 'API Error',
description: error.message,
});
throw error;
}
};
- Success Notifications
showNotification('success', 'Order Created', 'Order #12345 has been successfully created');
- Error Notifications
showNotification('error', 'API Error', 'Failed to process your request');
- Warning Notifications
showNotification('warning', 'Low Stock Alert', 'Product stock is below the minimum threshold');
- Info Notifications
showNotification('info', 'System Update', 'A system update is scheduled for tonight at 00:00');
// API error interceptor
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
// Success notification for POST, PUT, DELETE requests
if (
typeof window !== 'undefined' &&
['POST', 'PUT', 'DELETE'].includes(response.config.method?.toUpperCase() || '')
) {
const message =
response.config.method?.toUpperCase() === 'DELETE'
? 'Deletion successful!'
: 'Process completed successfully';
const showNotification = window.__showNotification as ShowNotificationFunction;
showNotification?.('success', message);
}
return response.data;
},
async (error: AxiosError) => {
if (error.response?.status === 401) {
if (typeof window !== 'undefined') {
await signOut({ callbackUrl: '/auth/login' });
}
}
const errorMessage = (error.response?.data as string) || error.message;
if (typeof window !== 'undefined') {
const showNotification = window.__showNotification as ShowNotificationFunction;
const truncatedMessage =
errorMessage.length > 500 ? errorMessage.slice(0, 497) + '...' : errorMessage;
showNotification?.('error', 'Hata', truncatedMessage);
}
throw new Error(errorMessage);
}
);
const handleFormSubmit = async (values: FormValues) => {
try {
await validateForm(values);
await submitForm(values);
showNotification('success', 'Form Submitted', 'Your form has been successfully submitted');
} catch (error) {
if (error instanceof ValidationError) {
showNotification('warning', 'Validation Error', error.message);
} else {
showNotification('error', 'Submission Error', 'Failed to submit form. Please try again');
}
}
};
The lib/auth/permissions.ts
file forms the core of the permission system:
// 1. Basic permission check
async function checkPermission(
resourceSlug: string,
actionSlug: string,
organizationId?: string
): Promise<boolean>;
// 2. Permission requirement
async function requirePermission(
resourceSlug: string,
actionSlug: string,
organizationId?: string
): Promise<void>;
// 3. React hook
function usePermission(resourceSlug: string, actionSlug: string, organizationId?: string): boolean;
// 4. Higher-order component
function withPermission(
Component: React.ComponentType<P>,
resourceSlug: string,
actionSlug: string
): React.FC<P>;
-
System Admin Check
if (session.user.role === 'ADMIN') return true;
-
Direct Permission Check
const hasDirectPermission = checkResourcePermission( session.user.permissions, resourceSlug, actionSlug );
-
Role-Based Permission Check
const rolePermissions = membership.role?.permissions || []; const hasRolePermission = checkResourcePermission(rolePermissions, resourceSlug, actionSlug);
-
Organization Permission Check
const organizationPermissions = membership.organization.permissions; return checkResourcePermission(organizationPermissions, resourceSlug, actionSlug);
// âś… CORRECT USAGE
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
// ... operations
}
// ❌ INCORRECT USAGE
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
const { id } = params; // missing await!
// ... operations
}
// 1. Single parameter
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
// ... operations
}
// 2. Multiple parameters
export async function GET(
request: NextRequest,
{
params,
}: {
params: Promise<{
organizationId: string;
userId: string;
}>;
}
) {
const { organizationId, userId } = await params;
// ... operations
}
// 3. Query parameters
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q');
// ... operations
}
params
in route handlers is a Promise- Use
Promise<{ id: string }>
in TypeScript definition - Always resolve
params
withawait
- This rule applies to all dynamic routes
- Pay attention to this rule to prevent build errors
The API client provides a type-safe way to make HTTP requests with built-in error handling and automatic notifications.
import { getRequest, postRequest, putRequest, deleteRequest } from '@/lib/apiClient';
// GET request
const users = await getRequest<User[]>('/users');
// POST request
const newUser = await postRequest<User>('/users', {
name: 'John',
email: 'john@example.com',
});
// PUT request
const updated = await putRequest<User>(`/users/${id}`, {
name: 'Updated Name',
});
// DELETE request
const deleted = await deleteRequest<boolean>(`/users/${id}`);
-
Automatic API Prefix
- All requests are automatically prefixed with
/api
- Example:
/users
becomes/api/users
- All requests are automatically prefixed with
-
Automatic Notifications
// Success notifications (for POST, PUT, DELETE) if (['POST', 'PUT', 'DELETE'].includes(response.config.method?.toUpperCase() || '')) { const message = response.config.method?.toUpperCase() === 'DELETE' ? 'Deletion successful!' : 'Process completed successfully'; showNotification?.('success', message); } // Error notifications showNotification({ type: 'error', message: 'API Error', description: errorMessage, });
- Success cases:
- Shows for POST/PUT/DELETE operations
- DELETE: "Deletion successful!"
- POST/PUT: "Process completed successfully"
- Error cases:
- Shows "API Error" as title
- Shows error message in description
- Success cases:
-
Retry Configuration
- 3 retry attempts for failed requests
- Uses exponential backoff
- Only retries on:
- Network errors
- 500+ server errors
- Does not retry if response contains error data
-
Type Safety
interface User { id: number; name: string; email: string; } // TypeScript will ensure type safety const user = await getRequest<User>('/users/1'); console.log(user.name); // TypeScript knows this exists
try {
const users = await getRequest<User[]>('/users');
// Success case - for POST/PUT/DELETE, automatically shows success notification
} catch (error) {
// Error notification is automatically shown:
// - Shows "Hata" with truncated error message
// - For 401, redirects to login page
console.error('API Error:', error);
}
interface RequestOptions {
headers?: Record<string, string>;
[key: string]: any; // Additional axios request config
}
// With custom headers
const response = await getRequest<User[]>('/users', {
headers: {
Authorization: 'Bearer token',
'Custom-Header': 'value',
},
});
// With query parameters
const filtered = await getRequest<User[]>('/users', {
role: 'admin',
active: true,
});
- Clone the repository
git clone https://github.com/yourusername/nextstarter.git
- Install dependencies
npm install
- Prepare the database
npm run reset-db # Create database schema
npm run seed # Load initial data
- Start the development server
npm run dev
- Fork the project
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'feat: add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Create a Pull Request
Create a .env
file in the project root directory and define the following variables:
# Database Connection
DB_HOST="localhost"
DB_PORT="5432"
DB_USER="postgres"
DB_PASSWORD="password"
DB_NAME="nextstarter"
# NextAuth.js Configuration
NEXTAUTH_SECRET="your-secret-key"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SESSION_MAX_AGE=604800 # 7 days (7 * 24 * 60 * 60)
NEXTAUTH_SESSION_UPDATE_AGE=3600 # 1 hour (60 * 60)
# Super Admin Account Details
SUPER_ADMIN_MAIL="superadmin@superadmin.com"
SUPER_ADMIN_FIRSTNAME="Super"
SUPER_ADMIN_LASTNAME="Admin"
SUPER_ADMIN_PASSWORD="your-secure-password"
-
Database Connection: The database configuration uses separate environment variables for host, port, user, password, and database name for Knex.js connection.
-
NextAuth.js Configuration:
NEXTAUTH_SECRET
: Secret key used for session securityNEXTAUTH_URL
: URL where the application runs (usuallyhttp://localhost:3000
in development)NEXTAUTH_SESSION_MAX_AGE
: Maximum session duration for session security (in seconds)NEXTAUTH_SESSION_UPDATE_AGE
: Session update duration for session security (in seconds)
-
Super Admin Account:
- System automatically creates a super admin account during initial setup
- Account details are taken from the
.env
file - This account has all system permissions
# Development
npm run dev # Start development server
npm run build # Build project for production
npm run start # Start production server
# Database
npm run migrate # Create and apply Knex migration
npm run reset-db # Reset database
npm run seed # Load seed data
# Code Quality
npm run lint # Code check with ESLint
npm run format # Format code with Prettier
The knex/seed.ts
file creates initial data:
-
Default Resources
// Example resources const defaultResources = [ { name: 'ALL', slug: '*' }, { name: 'ORGANIZATION', slug: 'organization' }, { name: 'USER', slug: 'user' }, // ... ];
-
Default Actions
const defaultActions = [ { name: 'VIEW', slug: 'view' }, { name: 'CREATE', slug: 'create' }, { name: 'EDIT', slug: 'edit' }, { name: 'DELETE', slug: 'delete' }, { name: 'MANAGE', slug: 'manage' }, ];
-
Super Admin and Roles
- Creates super admin user
- Defines default roles
- Assigns basic permissions