feat(codev): retire Surprise + QR public + mode admin suppr fiches
- carto.vue : retire bouton Surprise (Alliance seul reste), ajoute isAdmin + deleteFiche + colonne supprimer annuaire
- middleware : /codev/qr exempté d'authentification
- auth.post.ts : détecte mdp admin → pose cookie codev_admin
- DELETE /api/codev/fiches/[id] : vérifie cookie admin avant suppression NocoDB
- GET /api/codev/me : retourne { admin, session }
- nuxt.config.ts : codevAdminPassword ajouté
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,9 +14,10 @@ export default defineNuxtConfig({
|
|||||||
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
||||||
resendApiKey: process.env.RESEND_API_KEY,
|
resendApiKey: process.env.RESEND_API_KEY,
|
||||||
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
|
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
|
||||||
codevTableId: '', // NUXT_CODEV_TABLE_ID
|
codevTableId: '', // NUXT_CODEV_TABLE_ID
|
||||||
codevPassword: 'merci', // NUXT_CODEV_PASSWORD - défaut "merci", overridable
|
codevPassword: 'merci', // NUXT_CODEV_PASSWORD - défaut "merci", overridable
|
||||||
codevBaseId: '', // NUXT_CODEV_BASE_ID - base NocoDB (ex: pipilvsi7dibo80)
|
codevBaseId: '', // NUXT_CODEV_BASE_ID - base NocoDB (ex: pipilvsi7dibo80)
|
||||||
|
codevAdminPassword: 'admin2026', // NUXT_CODEV_ADMIN_PASSWORD
|
||||||
},
|
},
|
||||||
|
|
||||||
// Leaflet ne fonctionne pas en SSR — forcer le rendu côté client
|
// Leaflet ne fonctionne pas en SSR — forcer le rendu côté client
|
||||||
|
|||||||
@@ -61,15 +61,6 @@
|
|||||||
Alliance
|
Alliance
|
||||||
<span class="hint">besoins partages</span>
|
<span class="hint">besoins partages</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
:class="{ active: mode === 'surprise' }"
|
|
||||||
style="--mode-color: #3b82f6"
|
|
||||||
@click="setMode('surprise')"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Surprise
|
|
||||||
<span class="hint">offres partagees</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
v-if="mode !== 'none'"
|
v-if="mode !== 'none'"
|
||||||
class="reset"
|
class="reset"
|
||||||
@@ -94,6 +85,7 @@
|
|||||||
<th class="col-nom">Prénom</th>
|
<th class="col-nom">Prénom</th>
|
||||||
<th class="col-besoin">Besoin</th>
|
<th class="col-besoin">Besoin</th>
|
||||||
<th class="col-offre">Ce que j'offre</th>
|
<th class="col-offre">Ce que j'offre</th>
|
||||||
|
<th v-if="isAdmin" class="col-actions"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -101,6 +93,9 @@
|
|||||||
<td class="col-nom">{{ f.nom }}</td>
|
<td class="col-nom">{{ f.nom }}</td>
|
||||||
<td class="col-besoin">{{ f.besoin }}</td>
|
<td class="col-besoin">{{ f.besoin }}</td>
|
||||||
<td class="col-offre">{{ f.offre }}</td>
|
<td class="col-offre">{{ f.offre }}</td>
|
||||||
|
<td v-if="isAdmin" class="col-actions">
|
||||||
|
<button @click.stop="deleteFiche(f.id)" class="delete-btn" type="button" title="Supprimer">✕</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -145,7 +140,7 @@ import { computeMatches } from '~/utils/codev/matching'
|
|||||||
|
|
||||||
useHead({ title: 'Carto - Co-developpement' })
|
useHead({ title: 'Carto - Co-developpement' })
|
||||||
|
|
||||||
const { data, pending } = await useFetch<{ list: CodevFiche[] }>('/api/codev/fiches')
|
const { data, pending, refresh } = await useFetch<{ list: CodevFiche[] }>('/api/codev/fiches')
|
||||||
const fiches = computed(() => data.value?.list ?? [])
|
const fiches = computed(() => data.value?.list ?? [])
|
||||||
|
|
||||||
const matches = ref<CodevMatch[]>([])
|
const matches = ref<CodevMatch[]>([])
|
||||||
@@ -155,6 +150,15 @@ const tab = ref<'carto' | 'annuaire'>('carto')
|
|||||||
const selectedFiche = ref<CodevFiche | null>(null)
|
const selectedFiche = ref<CodevFiche | null>(null)
|
||||||
const isMobileView = typeof window !== 'undefined' ? window.innerWidth < 600 : false
|
const isMobileView = typeof window !== 'undefined' ? window.innerWidth < 600 : false
|
||||||
|
|
||||||
|
const isAdmin = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const r = await $fetch<{ admin: boolean }>('/api/codev/me')
|
||||||
|
isAdmin.value = r.admin
|
||||||
|
} catch { isAdmin.value = false }
|
||||||
|
})
|
||||||
|
|
||||||
const MODE_LABELS: Record<string, string> = {
|
const MODE_LABELS: Record<string, string> = {
|
||||||
solution: 'Solution',
|
solution: 'Solution',
|
||||||
alliance: 'Alliance',
|
alliance: 'Alliance',
|
||||||
@@ -177,6 +181,12 @@ function onSelectFiche(id: number) {
|
|||||||
navigateTo(`/codev/fiche?id=${id}`)
|
navigateTo(`/codev/fiche?id=${id}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteFiche(id: number) {
|
||||||
|
if (!confirm('Supprimer la fiche ?')) return
|
||||||
|
await $fetch(`/api/codev/fiches/${id}`, { method: 'DELETE' })
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -503,6 +513,19 @@ thead tr .col-nom { background: #f9fafb; }
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-actions { width: 40px; text-align: center; }
|
||||||
|
.delete-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
.delete-btn:hover { background: #fef2f2; }
|
||||||
|
|
||||||
/* ── Mobile ── */
|
/* ── Mobile ── */
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const expected = config.codevPassword || 'merci'
|
const expected = config.codevPassword || 'merci'
|
||||||
|
|
||||||
if (parsed.data.password.trim().toLowerCase() !== expected.trim().toLowerCase()) {
|
const isAdmin = parsed.data.password.trim().toLowerCase() === (config.codevAdminPassword || 'admin2026').trim().toLowerCase()
|
||||||
|
const isUser = parsed.data.password.trim().toLowerCase() === expected.trim().toLowerCase()
|
||||||
|
|
||||||
|
if (!isAdmin && !isUser) {
|
||||||
throw createError({ statusCode: 401, statusMessage: 'Mauvais mot de passe' })
|
throw createError({ statusCode: 401, statusMessage: 'Mauvais mot de passe' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cookie session (user + admin)
|
||||||
setCookie(event, 'codev_session', 'ok', {
|
setCookie(event, 'codev_session', 'ok', {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
@@ -27,5 +31,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
path: '/',
|
path: '/',
|
||||||
})
|
})
|
||||||
|
|
||||||
return { status: 200, ok: true }
|
// Cookie admin si mot de passe admin
|
||||||
|
if (isAdmin) {
|
||||||
|
setCookie(event, 'codev_admin', 'ok', {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
maxAge: 60 * 60 * 24, // 24h
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 200, ok: true, admin: isAdmin }
|
||||||
})
|
})
|
||||||
|
|||||||
25
server/api/codev/fiches/[id].delete.ts
Normal file
25
server/api/codev/fiches/[id].delete.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Vérif cookie admin
|
||||||
|
const adminCookie = getCookie(event, 'codev_admin')
|
||||||
|
if (adminCookie !== 'ok') {
|
||||||
|
throw createError({ statusCode: 403, statusMessage: 'Accès refusé' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const tableId = config.codevTableId
|
||||||
|
const id = getRouterParam(event, 'id')
|
||||||
|
|
||||||
|
if (!tableId || !id) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Parametre manquant' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await $fetch(`${config.nocodbUrl}/api/v2/tables/${tableId}/records`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'xc-token': config.nocodbToken, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ Id: Number(id) }),
|
||||||
|
}).catch(() => {
|
||||||
|
throw createError({ statusCode: 502, statusMessage: 'Erreur suppression' })
|
||||||
|
})
|
||||||
|
|
||||||
|
return { status: 200, ok: true }
|
||||||
|
})
|
||||||
5
server/api/codev/me.get.ts
Normal file
5
server/api/codev/me.get.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineEventHandler((event) => {
|
||||||
|
const admin = getCookie(event, 'codev_admin') === 'ok'
|
||||||
|
const session = getCookie(event, 'codev_session') === 'ok'
|
||||||
|
return { admin, session }
|
||||||
|
})
|
||||||
@@ -8,8 +8,9 @@ export default defineEventHandler((event) => {
|
|||||||
// Seulement les routes sous /codev/
|
// Seulement les routes sous /codev/
|
||||||
if (!path.startsWith('/codev/')) return
|
if (!path.startsWith('/codev/')) return
|
||||||
|
|
||||||
// Routes publiques : /codev/demo (et sous-routes éventuelles)
|
// Routes publiques : /codev/demo et /codev/qr (et sous-routes éventuelles)
|
||||||
if (path === '/codev/demo' || path.startsWith('/codev/demo/')) return
|
if (path === '/codev/demo' || path.startsWith('/codev/demo/')) return
|
||||||
|
if (path === '/codev/qr' || path.startsWith('/codev/qr/')) return
|
||||||
|
|
||||||
// Vérification cookie
|
// Vérification cookie
|
||||||
const session = getCookie(event, 'codev_session')
|
const session = getCookie(event, 'codev_session')
|
||||||
|
|||||||
Reference in New Issue
Block a user