security: add RBAC, HTTPS enforcement; add tests and CI pytest step

This commit is contained in:
TLimoges33
2025-08-28 17:29:16 +00:00
parent a2b8950d9a
commit f0c61de280
16 changed files with 271 additions and 198 deletions
+10 -10
View File
@@ -3,14 +3,14 @@ import Integrations from './Integrations'
import Guilds from './Guilds'
import Login from './Login'
export default function App(){
return (
<div style={{padding:20,fontFamily:'system-ui, sans-serif'}}>
<h1>LifeRPG Modern</h1>
<p>Welcome frontend scaffold. Connect to backend at <code>/api/v1</code>.</p>
<Login />
<Integrations />
<Guilds />
</div>
)
export default function App() {
return (
<div style={{ padding: 20, fontFamily: 'system-ui, sans-serif' }}>
<h1>LifeRPG Modern</h1>
<p>Welcome frontend scaffold. Connect to backend at <code>/api/v1</code>.</p>
<Login />
<Integrations />
<Guilds />
</div>
)
}
+47 -47
View File
@@ -1,55 +1,55 @@
import React, {useState, useEffect} from 'react'
import React, { useState, useEffect } from 'react'
const API = (path, opts) => fetch(path, {...(opts||{}), credentials: 'include'}).then(r=>r.json())
const API = (path, opts) => fetch(path, { ...(opts || {}), credentials: 'include' }).then(r => r.json())
export default function Guilds(){
const [guilds, setGuilds] = useState([])
const [name, setName] = useState('')
const [members, setMembers] = useState([])
const [selectedGuild, setSelectedGuild] = useState(null)
const [userId] = useState(1)
export default function Guilds() {
const [guilds, setGuilds] = useState([])
const [name, setName] = useState('')
const [members, setMembers] = useState([])
const [selectedGuild, setSelectedGuild] = useState(null)
const [userId] = useState(1)
useEffect(()=>{ API('/api/v1/guilds').then(setGuilds).catch(()=>setGuilds([])) }, [])
useEffect(() => { API('/api/v1/guilds').then(setGuilds).catch(() => setGuilds([])) }, [])
function createGuild(){
if(!name.trim()) return
API('/api/v1/guilds', {method:'POST', body: JSON.stringify({name, owner_id: userId}), headers: {'Content-Type':'application/json'}})
.then(g=> setGuilds([...guilds, g]))
.catch(()=>{})
}
function createGuild() {
if (!name.trim()) return
API('/api/v1/guilds', { method: 'POST', body: JSON.stringify({ name, owner_id: userId }), headers: { 'Content-Type': 'application/json' } })
.then(g => setGuilds([...guilds, g]))
.catch(() => { })
}
function loadMembers(gid){
API(`/api/v1/guilds/${gid}/members`).then(setMembers).catch(()=>setMembers([]))
setSelectedGuild(gid)
}
function loadMembers(gid) {
API(`/api/v1/guilds/${gid}/members`).then(setMembers).catch(() => setMembers([]))
setSelectedGuild(gid)
}
function addMember(gid){
const uid = prompt('User ID to add:')
if(!uid) return
API(`/api/v1/guilds/${gid}/members`, {method:'POST', body: JSON.stringify({user_id: parseInt(uid)}), headers: {'Content-Type':'application/json'}})
.then(()=> loadMembers(gid))
.catch(()=> alert('failed'))
}
function addMember(gid) {
const uid = prompt('User ID to add:')
if (!uid) return
API(`/api/v1/guilds/${gid}/members`, { method: 'POST', body: JSON.stringify({ user_id: parseInt(uid) }), headers: { 'Content-Type': 'application/json' } })
.then(() => loadMembers(gid))
.catch(() => alert('failed'))
}
return (
<div style={{marginTop:20}}>
<h2>Guilds</h2>
<div>
<input value={name} onChange={e=>setName(e.target.value)} placeholder="Guild name" />
<button onClick={createGuild} style={{marginLeft:8}}>Create</button>
</div>
<ul>
{guilds && guilds.length ? guilds.map(g=> (
<li key={g.id}>
<strong>{g.name}</strong> (owner: {g.owner_id}) <button onClick={()=>loadMembers(g.id)}>Members</button>
<button style={{marginLeft:8}} onClick={()=>addMember(g.id)}>Add Member</button>
</li>
)): <li>No guilds</li>}
</ul>
<h3>Members</h3>
<ul>
{members && members.length ? members.map(m=> (<li key={m.id}>User {m.user_id} {m.role}</li>)) : <li>No members</li>}
</ul>
</div>
)
return (
<div style={{ marginTop: 20 }}>
<h2>Guilds</h2>
<div>
<input value={name} onChange={e => setName(e.target.value)} placeholder="Guild name" />
<button onClick={createGuild} style={{ marginLeft: 8 }}>Create</button>
</div>
<ul>
{guilds && guilds.length ? guilds.map(g => (
<li key={g.id}>
<strong>{g.name}</strong> (owner: {g.owner_id}) <button onClick={() => loadMembers(g.id)}>Members</button>
<button style={{ marginLeft: 8 }} onClick={() => addMember(g.id)}>Add Member</button>
</li>
)) : <li>No guilds</li>}
</ul>
<h3>Members</h3>
<ul>
{members && members.length ? members.map(m => (<li key={m.id}>User {m.user_id} {m.role}</li>)) : <li>No members</li>}
</ul>
</div>
)
}
+76 -76
View File
@@ -1,86 +1,86 @@
import React, {useState, useEffect} from 'react'
import React, { useState, useEffect } from 'react'
const API = (path) => fetch(path, {credentials: 'include'}).then(r => r.json())
const API = (path) => fetch(path, { credentials: 'include' }).then(r => r.json())
export default function Integrations(){
const [integrations, setIntegrations] = useState([])
const [events, setEvents] = useState(null)
const [userId] = useState(1)
const [msg, setMsg] = useState(null)
const [loadingId, setLoadingId] = useState(null)
export default function Integrations() {
const [integrations, setIntegrations] = useState([])
const [events, setEvents] = useState(null)
const [userId] = useState(1)
const [msg, setMsg] = useState(null)
const [loadingId, setLoadingId] = useState(null)
useEffect(()=>{
API(`/api/v1/users/${userId}/integrations`).then(d=>setIntegrations(d)).catch(()=>setIntegrations([]))
}, [userId])
useEffect(() => {
API(`/api/v1/users/${userId}/integrations`).then(d => setIntegrations(d)).catch(() => setIntegrations([]))
}, [userId])
function startGoogle(){
// Open backend OAuth URL in new window so the redirect can complete
window.open(`/api/v1/oauth/google/login?user_id=${userId}`, '_blank')
}
function startGoogle() {
// Open backend OAuth URL in new window so the redirect can complete
window.open(`/api/v1/oauth/google/login?user_id=${userId}`, '_blank')
}
function fetchEvents(integrationId){
setLoadingId(integrationId)
fetch(`/api/v1/integrations/${integrationId}/google/events`, {credentials:'include'})
.then(r=>r.json())
.then(d=>{
setEvents(d)
setMsg('Fetched events')
})
.catch(e=>setEvents({error: String(e)}))
.finally(()=>setLoadingId(null))
}
function fetchEvents(integrationId) {
setLoadingId(integrationId)
fetch(`/api/v1/integrations/${integrationId}/google/events`, { credentials: 'include' })
.then(r => r.json())
.then(d => {
setEvents(d)
setMsg('Fetched events')
})
.catch(e => setEvents({ error: String(e) }))
.finally(() => setLoadingId(null))
}
function previewEvents(integrationId){
fetch(`/api/v1/integrations/${integrationId}/events_preview`, {credentials:'include'})
.then(r=>r.json()).then(d=>{
setEvents(d)
setMsg('Preview loaded')
}).catch(()=>setMsg('Preview failed'))
}
function previewEvents(integrationId) {
fetch(`/api/v1/integrations/${integrationId}/events_preview`, { credentials: 'include' })
.then(r => r.json()).then(d => {
setEvents(d)
setMsg('Preview loaded')
}).catch(() => setMsg('Preview failed'))
}
function removeIntegration(integrationId){
if(!confirm('Remove integration?')) return
setLoadingId(integrationId)
fetch(`/api/v1/integrations/${integrationId}`, {method: 'DELETE', credentials: 'include'})
.then(r=>r.json())
.then(d=>{
setMsg('Integration removed')
setIntegrations(integrations.filter(i=>i.id !== integrationId))
})
.catch(e=>setMsg('Failed to remove'))
.finally(()=>setLoadingId(null))
}
function removeIntegration(integrationId) {
if (!confirm('Remove integration?')) return
setLoadingId(integrationId)
fetch(`/api/v1/integrations/${integrationId}`, { method: 'DELETE', credentials: 'include' })
.then(r => r.json())
.then(d => {
setMsg('Integration removed')
setIntegrations(integrations.filter(i => i.id !== integrationId))
})
.catch(e => setMsg('Failed to remove'))
.finally(() => setLoadingId(null))
}
function syncIntegration(integrationId){
setLoadingId(integrationId)
fetch(`/api/v1/integrations/${integrationId}/sync_to_habits`, {method:'POST', credentials:'include'})
.then(r=>r.json())
.then(d=>setMsg(`Synced ${d.count || 0} items`))
.catch(e=>setMsg('Sync failed'))
.finally(()=>setLoadingId(null))
}
function syncIntegration(integrationId) {
setLoadingId(integrationId)
fetch(`/api/v1/integrations/${integrationId}/sync_to_habits`, { method: 'POST', credentials: 'include' })
.then(r => r.json())
.then(d => setMsg(`Synced ${d.count || 0} items`))
.catch(e => setMsg('Sync failed'))
.finally(() => setLoadingId(null))
}
return (
<div style={{marginTop:20}}>
<h2>Integrations</h2>
<button onClick={startGoogle}>Connect Google Calendar</button>
<h3>Your Integrations</h3>
<ul>
{integrations && integrations.length ? integrations.map(i=> (
<li key={i.id} style={{marginBottom:8}}>
<strong>{i.provider}</strong> id: {i.id} user: {i.user_id}
<div style={{display:'inline-block', marginLeft:12}}>
<button onClick={()=>fetchEvents(i.id)} disabled={loadingId===i.id} style={{marginRight:6}}>Fetch Events</button>
<button onClick={()=>previewEvents(i.id)} disabled={loadingId===i.id} style={{marginRight:6}}>Preview</button>
<button onClick={()=>syncIntegration(i.id)} disabled={loadingId===i.id} style={{marginRight:6}}>Sync Habits</button>
<button onClick={()=>removeIntegration(i.id)} disabled={loadingId===i.id}>Remove</button>
</div>
</li>
)): <li>No integrations</li>}
</ul>
{msg && <div style={{marginTop:8, color:'#0366d6'}}>{msg}</div>}
<h3>Events</h3>
<pre style={{whiteSpace:'pre-wrap',background:'#f6f6f6',padding:10}}>{events? JSON.stringify(events, null, 2): 'No events fetched'}</pre>
</div>
)
return (
<div style={{ marginTop: 20 }}>
<h2>Integrations</h2>
<button onClick={startGoogle}>Connect Google Calendar</button>
<h3>Your Integrations</h3>
<ul>
{integrations && integrations.length ? integrations.map(i => (
<li key={i.id} style={{ marginBottom: 8 }}>
<strong>{i.provider}</strong> id: {i.id} user: {i.user_id}
<div style={{ display: 'inline-block', marginLeft: 12 }}>
<button onClick={() => fetchEvents(i.id)} disabled={loadingId === i.id} style={{ marginRight: 6 }}>Fetch Events</button>
<button onClick={() => previewEvents(i.id)} disabled={loadingId === i.id} style={{ marginRight: 6 }}>Preview</button>
<button onClick={() => syncIntegration(i.id)} disabled={loadingId === i.id} style={{ marginRight: 6 }}>Sync Habits</button>
<button onClick={() => removeIntegration(i.id)} disabled={loadingId === i.id}>Remove</button>
</div>
</li>
)) : <li>No integrations</li>}
</ul>
{msg && <div style={{ marginTop: 8, color: '#0366d6' }}>{msg}</div>}
<h3>Events</h3>
<pre style={{ whiteSpace: 'pre-wrap', background: '#f6f6f6', padding: 10 }}>{events ? JSON.stringify(events, null, 2) : 'No events fetched'}</pre>
</div>
)
}
+21 -21
View File
@@ -1,25 +1,25 @@
import React, {useState} from 'react'
import React, { useState } from 'react'
export default function Login(){
const [email,setEmail]=useState('')
const [pw,setPw]=useState('')
const [msg,setMsg]=useState(null)
export default function Login() {
const [email, setEmail] = useState('')
const [pw, setPw] = useState('')
const [msg, setMsg] = useState(null)
function submit(e){
e.preventDefault()
fetch('/api/v1/auth/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email, password: pw}), credentials:'include'})
.then(r=>r.json()).then(()=> setMsg('Logged in')).catch(()=> setMsg('Login failed'))
}
function submit(e) {
e.preventDefault()
fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password: pw }), credentials: 'include' })
.then(r => r.json()).then(() => setMsg('Logged in')).catch(() => setMsg('Login failed'))
}
return (
<div style={{marginTop:20}}>
<h2>Login</h2>
<form onSubmit={submit}>
<div><input placeholder="email" value={email} onChange={e=>setEmail(e.target.value)} /></div>
<div><input placeholder="password" type="password" value={pw} onChange={e=>setPw(e.target.value)} /></div>
<button type="submit">Login</button>
</form>
{msg && <div style={{marginTop:8}}>{msg}</div>}
</div>
)
return (
<div style={{ marginTop: 20 }}>
<h2>Login</h2>
<form onSubmit={submit}>
<div><input placeholder="email" value={email} onChange={e => setEmail(e.target.value)} /></div>
<div><input placeholder="password" type="password" value={pw} onChange={e => setPw(e.target.value)} /></div>
<button type="submit">Login</button>
</form>
{msg && <div style={{ marginTop: 8 }}>{msg}</div>}
</div>
)
}
+3 -3
View File
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
<React.StrictMode>
<App />
</React.StrictMode>
)