Ignore local storage; admin users NIP-05, media, events, footer updates

- Add /storage/ and /backend/storage/ to .gitignore
- Track meetup time helper, logo asset, and assorted frontend/backend fixes
This commit is contained in:
bbe
2026-04-02 22:13:28 +02:00
parent 2fa378c360
commit 2ddf6495fb
13 changed files with 405 additions and 38 deletions

View File

@@ -7,8 +7,12 @@ import slugify from 'slugify';
import { prisma } from '../db/prisma';
import { requireAuth, requireRole } from '../middleware/auth';
const REPO_ROOT = path.resolve(__dirname, '../../..');
const STORAGE_PATH = process.env.MEDIA_STORAGE_PATH
|| path.resolve(__dirname, '../../../storage/media');
? path.isAbsolute(process.env.MEDIA_STORAGE_PATH)
? process.env.MEDIA_STORAGE_PATH
: path.resolve(REPO_ROOT, process.env.MEDIA_STORAGE_PATH)
: path.resolve(REPO_ROOT, 'storage/media');
function ensureStorageDir() {
fs.mkdirSync(STORAGE_PATH, { recursive: true });

View File

@@ -22,12 +22,17 @@ function getBlockedUsernames(): Set<string> {
const USERNAME_REGEX = /^[a-z0-9._-]+$/i;
function validateUsername(username: string): string | null {
function validateUsername(
username: string,
opts?: { allowReserved?: boolean }
): string | null {
if (!username || username.trim().length === 0) return 'Username is required';
if (username.length > 50) return 'Username must be 50 characters or fewer';
if (!USERNAME_REGEX.test(username)) return 'Username may only contain letters, numbers, dots, hyphens, and underscores';
const blocked = getBlockedUsernames();
if (blocked.has(username.toLowerCase())) return 'This username is reserved';
if (!opts?.allowReserved) {
const blocked = getBlockedUsernames();
if (blocked.has(username.toLowerCase())) return 'This username is reserved';
}
return null;
}
@@ -174,4 +179,58 @@ router.patch(
}
);
router.patch(
'/:pubkey',
requireAuth,
requireRole(['ADMIN']),
async (req: Request, res: Response) => {
try {
const pubkeyRaw = req.params.pubkey;
const pubkey =
typeof pubkeyRaw === 'string' ? pubkeyRaw : Array.isArray(pubkeyRaw) ? pubkeyRaw[0] : '';
if (!pubkey) {
res.status(400).json({ error: 'pubkey is required' });
return;
}
const { username } = req.body;
const normalized = (username as string || '').trim().toLowerCase();
const error = validateUsername(normalized, { allowReserved: true });
if (error) {
res.status(400).json({ error });
return;
}
const target = await prisma.user.findUnique({ where: { pubkey } });
if (!target) {
res.status(404).json({ error: 'User not found' });
return;
}
const existing = await prisma.user.findFirst({
where: {
username: { equals: normalized },
NOT: { pubkey },
},
});
if (existing) {
res.status(409).json({ error: 'Username is already taken' });
return;
}
const user = await prisma.user.update({
where: { pubkey },
data: { username: normalized },
});
res.json(user);
} catch (err) {
console.error('Admin update user username error:', err);
res.status(500).json({ error: 'Internal server error' });
}
}
);
export default router;