A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.

feat: make Global Twitter Config admin-only

- First registered user is admin and can configure Twitter cookies
- Additional users can only add/view their own account mappings
- Added /api/me endpoint to get current user info and admin status
- Protected twitter-config API routes with requireAdmin middleware
- Frontend hides Twitter config section for non-admin users

jack 650ea477 463d5374

+33 -11
+14 -6
public/index.html
··· 32 32 const [view, setView] = useState('login'); // login, register, dashboard 33 33 const [mappings, setMappings] = useState([]); 34 34 const [twitterConfig, setTwitterConfig] = useState({ authToken: '', ct0: '' }); 35 + const [isAdmin, setIsAdmin] = useState(false); 35 36 const [loading, setLoading] = useState(false); 36 37 const [error, setError] = useState(''); 37 38 ··· 45 46 const fetchData = async () => { 46 47 try { 47 48 const headers = { Authorization: `Bearer ${token}` }; 48 - const [mapRes, twitRes] = await Promise.all([ 49 - axios.get('/api/mappings', { headers }), 50 - axios.get('/api/twitter-config', { headers }) 49 + const [meRes, mapRes] = await Promise.all([ 50 + axios.get('/api/me', { headers }), 51 + axios.get('/api/mappings', { headers }) 51 52 ]); 53 + setIsAdmin(meRes.data.isAdmin); 52 54 setMappings(mapRes.data); 53 - setTwitterConfig(twitRes.data); 55 + if (meRes.data.isAdmin) { 56 + const twitRes = await axios.get('/api/twitter-config', { headers }); 57 + setTwitterConfig(twitRes.data); 58 + } 54 59 } catch (err) { 55 60 if (err.response?.status === 401) handleLogout(); 56 61 } ··· 66 71 }); 67 72 localStorage.setItem('token', res.data.token); 68 73 setToken(res.data.token); 74 + setIsAdmin(res.data.isAdmin); 69 75 } catch (err) { 70 76 setError('Invalid credentials'); 71 77 } ··· 181 187 182 188 <div className="container"> 183 189 <div className="row"> 184 - {/* Left Column: Twitter Config */} 190 + {/* Left Column: Twitter Config (Admin Only) */} 191 + {isAdmin && ( 185 192 <div className="col-md-4 mb-4"> 186 193 <div className="card p-3 mb-4"> 187 194 <h5 className="mb-3 d-flex align-items-center"> ··· 228 235 </form> 229 236 </div> 230 237 </div> 238 + )} 231 239 232 240 {/* Right Column: Active Mappings */} 233 - <div className="col-md-8"> 241 + <div className={isAdmin ? "col-md-8" : "col-md-12"}> 234 242 <div className="card p-3"> 235 243 <h5 className="mb-4 d-flex align-items-center"> 236 244 <span className="material-icons me-2">list</span> Active Sync Tasks
+19 -5
src/server.ts
··· 33 33 }); 34 34 }; 35 35 36 + // Middleware to require admin access 37 + const requireAdmin = (req: any, res: any, next: any) => { 38 + if (!req.user.isAdmin) { 39 + return res.status(403).json({ error: 'Admin access required' }); 40 + } 41 + next(); 42 + }; 43 + 36 44 // --- Auth Routes --- 37 45 38 46 app.post('/api/register', async (req, res) => { ··· 61 69 return; 62 70 } 63 71 64 - const token = jwt.sign({ email: user.email }, JWT_SECRET, { expiresIn: '24h' }); 65 - res.json({ token }); 72 + const userIndex = config.users.findIndex((u) => u.email === email); 73 + const isAdmin = userIndex === 0; 74 + const token = jwt.sign({ email: user.email, isAdmin }, JWT_SECRET, { expiresIn: '24h' }); 75 + res.json({ token, isAdmin }); 76 + }); 77 + 78 + app.get('/api/me', authenticateToken, (req: any, res) => { 79 + res.json({ email: req.user.email, isAdmin: req.user.isAdmin }); 66 80 }); 67 81 68 82 // --- Mapping Routes --- ··· 99 113 res.json({ success: true }); 100 114 }); 101 115 102 - // --- Twitter Config Routes --- 116 + // --- Twitter Config Routes (Admin Only) --- 103 117 104 - app.get('/api/twitter-config', authenticateToken, (_req, res) => { 118 + app.get('/api/twitter-config', authenticateToken, requireAdmin, (_req, res) => { 105 119 const config = getConfig(); 106 120 res.json(config.twitter); 107 121 }); 108 122 109 - app.post('/api/twitter-config', authenticateToken, (req, res) => { 123 + app.post('/api/twitter-config', authenticateToken, requireAdmin, (req, res) => { 110 124 const { authToken, ct0 } = req.body; 111 125 const config = getConfig(); 112 126 config.twitter = { authToken, ct0 };