cronjob de manejo de invitados listo
This commit is contained in:
@@ -325,7 +325,7 @@ function openAddUser() {
|
||||
}
|
||||
function openAddGuest() {
|
||||
userFormMode.value = 'guest';
|
||||
userFormModel.value = { username:'', password:'', vlan:'5', disabled:false };
|
||||
userFormModel.value = { username:'', password:'', vlan:'5', disabled:false, etiquetas: ['invitado'] };
|
||||
showUserForm.value = true;
|
||||
}
|
||||
function toggleSettingsMenu() { showSettingsMenu.value = !showSettingsMenu.value; }
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<button class="icon-btn" @click="$emit('remove', item)">Eliminar</button>
|
||||
<button v-if="expandable" class="icon-btn" @click="$emit('toggleExpand')">{{ expanded ? 'Contraer' : 'Expandir' }}</button>
|
||||
</div>
|
||||
<div v-if="item.etiquetas && item.etiquetas.length" class="row" style="gap:6px; margin-top:6px;">
|
||||
<span v-for="tag in item.etiquetas" :key="tag" class="chip">#{{ tag }}</span>
|
||||
</div>
|
||||
<div v-if="expanded && deviceList.length" style="margin-top:8px;">
|
||||
<div class="grid">
|
||||
<DispositivoCard v-for="d in deviceList" :key="d.id" :device="d" :connected="isConnected(d.id)" simple @edit="$emit('editDevice', d)" />
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
<input v-model="state.password" placeholder="contraseña" style="width:100%; background:transparent; border:none; outline:none; color:inherit;"/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="toggle" style="flex:1;">
|
||||
<div class="muted" style="font-size:12px;">Etiquetas (separadas por coma)</div>
|
||||
<input v-model="etiquetasText" placeholder="invitado, vip" style="width:100%; background:transparent; border:none; outline:none; color:inherit;" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="toggle" style="flex:1;">
|
||||
<div class="muted" style="font-size:12px;">VLAN</div>
|
||||
@@ -28,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch, computed } from 'vue';
|
||||
import { reactive, watch, computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, default: () => ({ username:'', password:'', vlan:'', disabled:false }) },
|
||||
@@ -38,13 +44,16 @@ const emit = defineEmits(['update:modelValue', 'submit', 'cancel']);
|
||||
|
||||
const state = reactive({ username:'', password:'', vlan:'', disabled:false });
|
||||
const isEdit = computed(() => props.mode === 'edit');
|
||||
const etiquetasText = ref('');
|
||||
|
||||
watch(() => props.modelValue, (v) => {
|
||||
Object.assign(state, v || {});
|
||||
const tags = Array.isArray(v?.etiquetas) ? v.etiquetas : [];
|
||||
etiquetasText.value = tags.join(', ');
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
function submit() {
|
||||
emit('submit', { ...state });
|
||||
const tags = etiquetasText.value.split(',').map(s => s.trim()).filter(Boolean).slice(0, 100);
|
||||
emit('submit', { ...state, etiquetas: tags });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createApp } from './src/app.js';
|
||||
import { ensureSchema } from './src/services/db.js';
|
||||
import { ensureSchema, disableGuestsFromYesterday } from './src/services/db.js';
|
||||
|
||||
const app = createApp();
|
||||
const port = process.env.PORT || 3000;
|
||||
@@ -13,3 +13,24 @@ try {
|
||||
app.listen(port, () => {
|
||||
console.log(`Node RADIUS REST API listening on :${port}`);
|
||||
});
|
||||
|
||||
// Schedule daily guest disable at 4:00 AM America/Tegucigalpa (UTC-6 -> 10:00 UTC)
|
||||
function scheduleGuestJob() {
|
||||
const now = new Date();
|
||||
const next = new Date(now);
|
||||
next.setUTCHours(10, 0, 0, 0); // 10:00 UTC == 4:00 local (UTC-6)
|
||||
if (next <= now) next.setUTCDate(next.getUTCDate() + 1);
|
||||
const delay = next - now;
|
||||
setTimeout(async function run() {
|
||||
try {
|
||||
const { count } = await disableGuestsFromYesterday();
|
||||
if (count) console.log(`[guest-cron] Disabled ${count} invitado users from yesterday`);
|
||||
} catch (e) {
|
||||
console.error('[guest-cron] Error:', e?.message || e);
|
||||
} finally {
|
||||
scheduleGuestJob();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
scheduleGuestJob();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import { VLAN_ID } from '../config/env.js';
|
||||
import { clearRequests, getRecentRequests, registerSse } from '../sse.js';
|
||||
import { deleteUserFromDb, readUsersFromDb, upsertUserToDb, pool } from '../services/db.js';
|
||||
import { deleteUserFromDb, readUsersFromDb, upsertUserToDb, pool, disableGuestsFromYesterday } from '../services/db.js';
|
||||
import { disconnectUserSessions } from '../services/radius.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -17,17 +17,31 @@ router.get('/users', async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/users/:username', async (req, res) => {
|
||||
try {
|
||||
const uname = String(req.params.username);
|
||||
const items = await readUsersFromDb();
|
||||
const found = items.find(u => u.username === uname);
|
||||
if (!found) return res.status(404).json({ ok: false, error: 'not_found' });
|
||||
res.json({ ok: true, item: found });
|
||||
} catch (e) {
|
||||
console.error('GET /api/users/:username error:', e?.message || e);
|
||||
res.status(500).json({ ok: false, error: 'db_error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/users', async (req, res) => {
|
||||
const { username, password, vlan, disabled } = req.body || {};
|
||||
const { username, password, vlan, disabled, etiquetas } = req.body || {};
|
||||
if (!username || !password) return res.status(400).json({ ok: false, error: 'username and password required' });
|
||||
const user = { username: String(username), password: String(password), vlan: String(vlan || VLAN_ID), disabled: !!disabled };
|
||||
if (Array.isArray(etiquetas)) user.etiquetas = etiquetas.map(String).slice(0, 100);
|
||||
await upsertUserToDb(user);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.patch('/users/:username', async (req, res) => {
|
||||
const uname = String(req.params.username);
|
||||
const { password, vlan, disabled } = req.body || {};
|
||||
const { password, vlan, disabled, etiquetas } = req.body || {};
|
||||
const current = (await readUsersFromDb()).find(u => u.username === uname);
|
||||
if (!current) return res.status(404).json({ ok: false, error: 'not_found' });
|
||||
const next = {
|
||||
@@ -36,6 +50,7 @@ router.patch('/users/:username', async (req, res) => {
|
||||
vlan: vlan !== undefined ? String(vlan) : current.vlan,
|
||||
disabled: disabled !== undefined ? !!disabled : current.disabled,
|
||||
};
|
||||
if (etiquetas !== undefined) next.etiquetas = Array.isArray(etiquetas) ? etiquetas.map(String).slice(0, 100) : current.etiquetas;
|
||||
await upsertUserToDb(next);
|
||||
if (disabled === true) {
|
||||
disconnectUserSessions(uname).catch(err => console.error('CoA disconnect error:', err));
|
||||
@@ -202,4 +217,15 @@ router.post('/devices/:id/disconnect', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Manual trigger: disable invited users from yesterday (America/Tegucigalpa day)
|
||||
router.post('/guests/disable-yesterday', async (_req, res) => {
|
||||
try {
|
||||
const { count } = await disableGuestsFromYesterday();
|
||||
res.json({ ok: true, count });
|
||||
} catch (e) {
|
||||
console.error('POST /api/guests/disable-yesterday error:', e?.message || e);
|
||||
res.status(500).json({ ok: false, error: 'db_error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -176,7 +176,7 @@ export async function readUsersFromDb() {
|
||||
}
|
||||
|
||||
export async function upsertUserToDb(user) {
|
||||
const { username, password, vlan, disabled } = user;
|
||||
const { username, password, vlan, disabled, etiquetas } = user;
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
@@ -185,6 +185,9 @@ export async function upsertUserToDb(user) {
|
||||
'INSERT INTO users (username) VALUES ($1) ON CONFLICT (username) DO NOTHING',
|
||||
[username]
|
||||
);
|
||||
if (Array.isArray(etiquetas)) {
|
||||
await client.query('UPDATE users SET etiquetas = $2, updated_at = NOW() WHERE username = $1', [username, etiquetas.map(String).slice(0, 100)]);
|
||||
}
|
||||
await client.query("DELETE FROM radcheck WHERE username = $1 AND attribute = 'Cleartext-Password'", [username]);
|
||||
await client.query(
|
||||
"INSERT INTO radcheck (username, attribute, op, value) VALUES ($1,'Cleartext-Password',':=',$2)",
|
||||
@@ -234,6 +237,37 @@ export async function upsertUserToDb(user) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function disableGuestsFromYesterday() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const { rows } = await client.query(`
|
||||
WITH tz AS (
|
||||
SELECT (NOW() AT TIME ZONE 'America/Tegucigalpa')::date AS today
|
||||
)
|
||||
SELECT u.username
|
||||
FROM users u, tz
|
||||
WHERE 'invitado' = ANY(u.etiquetas)
|
||||
AND u.habilitado_since IS NOT NULL
|
||||
AND (u.habilitado_since AT TIME ZONE 'America/Tegucigalpa')::date = (tz.today - INTERVAL '1 day')::date
|
||||
`);
|
||||
const usernames = rows.map(r => r.username);
|
||||
if (usernames.length === 0) { await client.query('COMMIT'); return { count: 0 }; }
|
||||
await client.query("DELETE FROM radcheck WHERE attribute = 'Auth-Type' AND username = ANY($1)", [usernames]);
|
||||
const values = usernames.map(u => `('${u}','Auth-Type',':=','Reject')`).join(',');
|
||||
await client.query(`INSERT INTO radcheck (username, attribute, op, value) VALUES ${values}`);
|
||||
await client.query('UPDATE users SET updated_at = NOW() WHERE username = ANY($1)', [usernames]);
|
||||
await client.query('COMMIT');
|
||||
return { count: usernames.length };
|
||||
} catch (e) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('disableGuestsFromYesterday error:', e?.message || e);
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUserFromDb(username) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user