Skip to content

Commit

Permalink
Added disable accounts option
Browse files Browse the repository at this point in the history
  • Loading branch information
jordan-dalby committed Nov 24, 2024
1 parent 88a17e6 commit 8f7d19f
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 21 deletions.
4 changes: 4 additions & 0 deletions client/src/components/auth/UserDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const UserDropdown: React.FC = () => {
const { user, logout } = useAuth();
const location = useLocation();

if (user?.id === 0) {
return (<></>)
}

useOutsideClick(dropdownRef, () => setIsOpen(false));

const isPublicView = location.pathname.startsWith('/public');
Expand Down
2 changes: 1 addition & 1 deletion client/src/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ export const DEFAULT_SETTINGS = {
showLineNumbers: true,
} as const;

export const APP_VERSION = '1.5.2';
export const APP_VERSION = '1.5.3';
30 changes: 21 additions & 9 deletions client/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { createContext, useState, useEffect } from 'react';
import { useToast } from '../hooks/useToast';
import { EVENTS } from '../constants/events';
import { getAuthConfig, verifyToken } from '../utils/api/auth';
import { anonymous, getAuthConfig, verifyToken } from '../utils/api/auth';
import type { User, AuthConfig } from '../types/user';

interface AuthContextType {
Expand Down Expand Up @@ -44,14 +44,26 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const config = await getAuthConfig();
setAuthConfig(config);

const token = localStorage.getItem('token');
if (token) {
const response = await verifyToken();
if (response.valid && response.user) {
setIsAuthenticated(true);
setUser(response.user);
} else {
localStorage.removeItem('token');
if (config.disableAccounts) {
try {
const response = await anonymous();
if (response.token && response.user) {
login(response.token, response.user);
}
} catch (error) {
console.error('Failed to create anonymous session:', error);
addToast('Failed to initialize anonymous session', 'error');
}
} else {
const token = localStorage.getItem('token');
if (token) {
const response = await verifyToken();
if (response.valid && response.user) {
setIsAuthenticated(true);
setUser(response.user);
} else {
localStorage.removeItem('token');
}
}
}
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions client/src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export interface AuthConfig {
authRequired: boolean;
allowNewAccounts: boolean;
hasUsers: boolean;
disableAccounts: boolean;
}
6 changes: 5 additions & 1 deletion client/src/utils/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ export const login = async (username: string, password: string): Promise<AuthRes

export const register = async (username: string, password: string): Promise<AuthResponse> => {
return apiClient.post<AuthResponse>(`${API_ENDPOINTS.AUTH}/register`, { username, password });
};
};

export const anonymous = async (): Promise<AuthResponse> => {
return apiClient.post<AuthResponse>(`${API_ENDPOINTS.AUTH}/anonymous`, {});
}
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ services:
- ALLOW_NEW_ACCOUNTS=true
# Should debug mode be enabled? Essentially enables logging, in most cases leave this as false
- DEBUG=false
# Should we use accounts at all? When enabled, it will be like starting a fresh account so export your snippets, no login required
- DISABLE_ACCOUNTS=false

# Optional: Enable OIDC for Single Sign On
- OIDC_ENABLED=true
Expand Down
45 changes: 43 additions & 2 deletions server/src/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import fs from 'fs';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import Logger from '../logger.js';
import userRepository from '../repositories/userRepository.js';

function getJwtSecret() {
if (process.env.JWT_SECRET_FILE) {
Expand All @@ -16,8 +19,39 @@ function getJwtSecret() {
const JWT_SECRET = getJwtSecret();
const ALLOW_NEW_ACCOUNTS = process.env.ALLOW_NEW_ACCOUNTS === 'true';
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
const DISABLE_ACCOUNTS = process.env.DISABLE_ACCOUNTS === 'true';

function generateAnonymousUsername() {
return `anon-${crypto.randomBytes(8).toString('hex')}`;
}

async function getOrCreateAnonymousUser() {
try {
let existingUser = await userRepository.findById(0);

if (existingUser) {
return existingUser;
}

return await userRepository.createAnonymousUser(generateAnonymousUsername());
} catch (error) {
Logger.error('Error getting/creating anonymous user:', error);
throw error;
}
}

const authenticateToken = async (req, res, next) => {
if (DISABLE_ACCOUNTS) {
try {
const anonymousUser = await getOrCreateAnonymousUser();
req.user = anonymousUser;
return next();
} catch (error) {
Logger.error('Error in anonymous authentication:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}

const authenticateToken = (req, res, next) => {
const authHeader = req.headers['bytestashauth'];
const token = authHeader && authHeader.split(' ')[1];

Expand All @@ -34,4 +68,11 @@ const authenticateToken = (req, res, next) => {
});
};

export { authenticateToken, JWT_SECRET, TOKEN_EXPIRY, ALLOW_NEW_ACCOUNTS };
export {
authenticateToken,
JWT_SECRET,
TOKEN_EXPIRY,
ALLOW_NEW_ACCOUNTS,
DISABLE_ACCOUNTS,
getOrCreateAnonymousUser
};
8 changes: 4 additions & 4 deletions server/src/repositories/snippetRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class SnippetRepository {
FROM snippets s
LEFT JOIN categories c ON s.id = c.snippet_id
LEFT JOIN users u ON s.user_id = u.id
WHERE s.is_public = TRUE
WHERE s.is_public = 1
GROUP BY s.id
ORDER BY s.updated_at DESC
`);
Expand Down Expand Up @@ -126,7 +126,7 @@ class SnippetRepository {
FROM snippets s
LEFT JOIN categories c ON s.id = c.snippet_id
LEFT JOIN users u ON s.user_id = u.id
WHERE s.id = ? AND (s.user_id = ? OR s.is_public = TRUE)
WHERE s.id = ? AND (s.user_id = ? OR s.is_public = 1)
GROUP BY s.id
`);

Expand Down Expand Up @@ -197,7 +197,7 @@ class SnippetRepository {
}
}

create({ title, description, categories = [], fragments = [], userId, isPublic = false }) {
create({ title, description, categories = [], fragments = [], userId, isPublic = 0 }) {
this.#initializeStatements();
try {
const db = getDb();
Expand Down Expand Up @@ -290,7 +290,7 @@ class SnippetRepository {
findById(id, userId = null) {
this.#initializeStatements();
try {
if (userId) {
if (userId != null) {
const snippet = this.selectByIdStmt.get(id, userId);
return this.#processSnippet(snippet);
}
Expand Down
31 changes: 31 additions & 0 deletions server/src/repositories/userRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ class UserRepository {
FROM users
WHERE username_normalized = ? COLLATE NOCASE
`);

this.createAnonymousUserStmt = db.prepare(`
INSERT INTO users (
id,
username,
username_normalized,
password_hash,
created_at
) VALUES (0, ?, ?, '', datetime('now'))
ON CONFLICT(id) DO NOTHING
`);
}
}

Expand Down Expand Up @@ -144,6 +155,26 @@ class UserRepository {
this.#initializeStatements();
return this.findByOIDCIdStmt.get(oidcId, provider);
}

async createAnonymousUser(username) {
this.#initializeStatements();

try {
this.createAnonymousUserStmt.run(
username,
username.toLowerCase()
);

return {
id: 0,
username,
created_at: new Date().toISOString()
};
} catch (error) {
Logger.error('Error creating anonymous user:', error);
throw error;
}
}
}

export default new UserRepository();
28 changes: 25 additions & 3 deletions server/src/routes/authRoutes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import { JWT_SECRET, TOKEN_EXPIRY, ALLOW_NEW_ACCOUNTS } from '../middleware/auth.js';
import { JWT_SECRET, TOKEN_EXPIRY, ALLOW_NEW_ACCOUNTS, DISABLE_ACCOUNTS, getOrCreateAnonymousUser } from '../middleware/auth.js';
import userService from '../services/userService.js';
import { getDb } from '../config/database.js';
import { up_v1_5_0_snippets } from '../config/migrations/20241117-migration.js';
Expand All @@ -16,8 +16,9 @@ router.get('/config', async (req, res) => {

res.json({
authRequired: true,
allowNewAccounts: !hasUsers || ALLOW_NEW_ACCOUNTS,
hasUsers
allowNewAccounts: (!hasUsers || ALLOW_NEW_ACCOUNTS) && !DISABLE_ACCOUNTS,
hasUsers,
disableAccounts: DISABLE_ACCOUNTS
});
} catch (error) {
Logger.error('Error getting auth config:', error);
Expand Down Expand Up @@ -115,4 +116,25 @@ router.get('/verify', async (req, res) => {
}
});

router.post('/anonymous', async (req, res) => {
if (!DISABLE_ACCOUNTS) {
return res.status(403).json({ error: 'Anonymous login not allowed' });
}

try {
const anonymousUser = await getOrCreateAnonymousUser();
const token = jwt.sign({
id: anonymousUser.id,
username: anonymousUser.username
}, JWT_SECRET, {
expiresIn: TOKEN_EXPIRY
});

res.json({ token, user: anonymousUser });
} catch (error) {
Logger.error('Error in anonymous login:', error);
res.status(500).json({ error: 'Failed to create anonymous session' });
}
});

export default router;
2 changes: 1 addition & 1 deletion server/src/services/snippetService.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class SnippetService {

async findById(id, userId = null) {
try {
Logger.debug('Service: Getting snippet:', id, userId ? `for user: ${userId}` : '(public access)');
Logger.debug('Service: Getting snippet:', id, userId != null ? `for user: ${userId}` : '(public access)');
const result = await snippetRepository.findById(id, userId);
Logger.debug('Service: Find by ID result:', result ? 'Found' : 'Not Found');
return result;
Expand Down

0 comments on commit 8f7d19f

Please sign in to comment.