first commit
Made-with: Cursor
This commit is contained in:
90
backend/src/services/auth.ts
Normal file
90
backend/src/services/auth.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { verifyEvent, type VerifiedEvent, nip19 } from 'nostr-tools';
|
||||
import { prisma } from '../db/prisma';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production';
|
||||
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
interface StoredChallenge {
|
||||
challenge: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
const challenges = new Map<string, StoredChallenge>();
|
||||
|
||||
// Periodically clean up expired challenges
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of challenges) {
|
||||
if (value.expiresAt < now) {
|
||||
challenges.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
export const authService = {
|
||||
createChallenge(pubkey: string): string {
|
||||
const challenge = uuidv4();
|
||||
challenges.set(pubkey, {
|
||||
challenge,
|
||||
expiresAt: Date.now() + CHALLENGE_TTL_MS,
|
||||
});
|
||||
return challenge;
|
||||
},
|
||||
|
||||
verifySignature(pubkey: string, signedEvent: VerifiedEvent): boolean {
|
||||
const stored = challenges.get(pubkey);
|
||||
if (!stored) return false;
|
||||
if (stored.expiresAt < Date.now()) {
|
||||
challenges.delete(pubkey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the event signature
|
||||
if (!verifyEvent(signedEvent)) return false;
|
||||
|
||||
// Kind 22242 is the NIP-42 auth kind
|
||||
if (signedEvent.kind !== 22242) return false;
|
||||
if (signedEvent.pubkey !== pubkey) return false;
|
||||
|
||||
// Check that the challenge tag matches
|
||||
const challengeTag = signedEvent.tags.find(
|
||||
(t) => t[0] === 'challenge'
|
||||
);
|
||||
if (!challengeTag || challengeTag[1] !== stored.challenge) return false;
|
||||
|
||||
challenges.delete(pubkey);
|
||||
return true;
|
||||
},
|
||||
|
||||
generateToken(pubkey: string, role: string): string {
|
||||
return jwt.sign({ pubkey, role }, JWT_SECRET, { expiresIn: '7d' });
|
||||
},
|
||||
|
||||
async getRole(pubkey: string): Promise<string> {
|
||||
const adminPubkeys = (process.env.ADMIN_PUBKEYS || '')
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
.map((p) => {
|
||||
if (p.startsWith('npub1')) {
|
||||
try {
|
||||
const { data } = nip19.decode(p);
|
||||
return data as string;
|
||||
} catch {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
if (adminPubkeys.includes(pubkey)) return 'ADMIN';
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { pubkey } });
|
||||
if (user?.role === 'MODERATOR') return 'MODERATOR';
|
||||
if (user?.role === 'ADMIN') return 'ADMIN';
|
||||
|
||||
return 'USER';
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user