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