<!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta name='author' content='Anton Yaky'> <meta name='viewport' content='width=device-width, initial-scale=1'> <title>simple-synapse-admin</title> <script> window.onload = (event) => { _el('homeserver').value = localStorage.getItem('homeserver'); _el('access_token').value = localStorage.getItem('access_token'); } function _el(id) { return document.getElementById(id); } function _val(id) { return document.getElementById(id).value; } function credentialsSave() { let homeserver = _val('homeserver'); localStorage.setItem('homeserver', homeserver); let access_token = _val('access_token'); localStorage.setItem('access_token', access_token) } function credentialsClear() { _el('homeserver').value = ''; _el('access_token').value = ''; localStorage.clear(); } function usersList(users_from) { const homeserver = _val('homeserver'); if (!homeserver) { alert('Homeserver URL is required'); return; } const access_token = _val('access_token'); if (!access_token) { alert('Access token is required'); return; } fetch(`${homeserver}/_synapse/admin/v2/users?from=${users_from}&guests=true&deactivated=true&limit=20`, { method: 'GET', headers: { 'Authorization': `Bearer ${access_token}` } }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); return response.json(); }) .then((data) => { _el('users_total').innerText = `Total users: ${data.total}`; if (data.next_token != undefined) { _el('users_next').setAttribute('onclick', `usersList(${data.next_token})`); _el('users_next').disabled = false; } else { _el('users_next').disabled = true; } let tableHtml = ` <tr> <th>ID</th> <th>Display Name</th> <th>Guest</th> <th>Admin</th> <th>Type</th> <th>Deactivated</th> <th>Erased</th> <th>Shadowbanned</th> <th>Avatar URL</th> <th>Created On</th> <th></th> <th></th> </tr>`; for (let i = 0; i < data.users.length; i++) { let user = data.users[i]; tableHtml += ` <tr> <td>${user.name}</td> <td>${user.displayname}</td> <td>${user.is_guest ? 'Y' : ''}</td> <td>${user.admin ? 'Y' : ''}</td> <td>${user.user_type ? 'Y' : ''}</td> <td id='user_deactivated_${i}'>${user.deactivated ? 'Y' : ''}</td> <td>${user.erased ? 'Y' : ''}</td> <td>${user.shadow_banned ? 'Y' : ''}</td> <td>${user.avatar_url}</td> <td>${new Date(user.creation_ts)}</td> <td><button onclick='userResetPassword(${i},"${user.name}")'>Reset password</button></td> <td><button id='user_deactivate_${i}' onclick='userDeactivate(${i},"${user.name}")'${user.deactivated ? ' disabled' : ''}>Deactivate</button></td> </tr>`; }; _el('users_table').innerHTML = tableHtml; }) .catch((errorText) => { alert(`Error getting a list of users: ${errorText}`); }); } function userResetPassword(i,user_id) { const new_password = prompt('Enter a new password'); const homeserver = _val('homeserver'); const access_token = _val('access_token'); fetch(`${homeserver}/_synapse/admin/v1/reset_password/${user_id}`, { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ 'new_password': new_password, 'logout_devices': true }) }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); alert(`Password for user ${user_id} was successfully reset.`); }) .catch((errorText) => { alert(`Error resetting password for user ${user_id}: ${errorText}.`); }); } function userDeactivate(i,user_id) { if (!confirm(`Are you sure you want to deactivate user ${user_id}?`)) return; const homeserver = _val('homeserver'); const access_token = _val('access_token'); fetch(`${homeserver}/_synapse/admin/v1/deactivate/${user_id}`, { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ 'erase': true }) }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); _el(`user_deactivated_${i}`).innerText = 'Y'; _el(`user_deactivate_${i}`).disabled = true; alert(`User ${user_id} was successfully deactivated and erased. Synapse cannot delete user accounts completely.`) }) .catch((errorText) => { alert(`Error deactivating user ${user_id}: ${errorText}.`); }); } function roomsList(rooms_from) { const homeserver = _val('homeserver'); const access_token = _val('access_token'); if (!homeserver) { alert('Homeserver URL is required'); return; } if (!access_token) { alert('Access token is required'); return; } let url = `${homeserver}/_synapse/admin/v1/rooms?from=${rooms_from}&limit=20`; const search_term = _val('search_term'); if (search_term) { url += `&search_term=${search_term}` } fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${access_token}` } }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); return response.json(); }) .then((data) => { _el('rooms_total').innerText = `Total rooms: ${data.total_rooms}`; if (data.prev_batch != undefined) { _el('rooms_prev').setAttribute('onclick', `roomsList(${data.prev_batch})`) _el('rooms_prev').disabled = false; } else { _el('rooms_prev').disabled = true; } if (data.next_batch != undefined) { _el('rooms_next').setAttribute('onclick', `roomsList(${data.next_batch})`) _el('rooms_next').disabled = false; } else { _el('rooms_next').disabled = true; } let tableHtml = ` <tr> <th>ID</th> <th>Name</th> <th>Encryption</th> <th>Creator</th> <th>Members</th> <th>Local Members</th> <th>Local Members List</th> <th></th> </tr>`; for (let i = 0; i < data.rooms.length; i++) { const room = data.rooms[i]; tableHtml += ` <tr> <td>${room.room_id}</td> <td>${room.name}</td> <td>${room.encryption}</td> <td>${room.creator}</td> <td>${room.joined_members}</td> <td>${room.joined_local_members}</td> <td id='room_members_${i}'><button onclick='roomGetMembers(${i},"${room.room_id}")'>Members</button></td> <td id='room_delete_state_${i}'><button id='room_delete_${i}' onclick='roomDelete(${i},"${room.room_id}",false)'>Delete</buton></td> </tr>`; }; _el('rooms_table').innerHTML = tableHtml; }) .catch((errorText) => { alert(`Error getting a list of rooms: ${errorText}`); }); } function roomGetMembers(i, room_id) { const homeserver = _val('homeserver'); const access_token = _val('access_token'); fetch(`${homeserver}/_synapse/admin/v1/rooms/${room_id}/members`, { method: 'GET', headers: { 'Authorization': `Bearer ${access_token}` } }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); return response.json(); }) .then((data) => { let membersHtml = '<ul>'; for (member of data.members) { membersHtml += `<li>${member}</li>`; } membersHtml += '</ul>'; _el(`room_members_${i}`).innerHTML = membersHtml; }) .catch((errorText) => { alert(`Error getting a list of members for room ${room_id}: ${errorText}`); }); } function roomDelete(i, room_id, force) { const homeserver = _val('homeserver'); const access_token = _val('access_token'); fetch(`${homeserver}/_synapse/admin/v1/rooms/${room_id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ 'purge': true, 'force_purge': (force === true) }) }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); _el(`room_delete_state_${i}`).innerHTML = 'DELETED'; alert(`Room ${room_id} successfully deleted.`); }) .catch((errorText) => { alert(`Error deleting room ${room_id}: ${errorText}. You can try to force purge the room.`); _el(`room_delete_${i}`).setAttribute('onclick', `roomDelete(${i},"${room_id}",true)`); _el(`room_delete_${i}`).innerText = 'Force purge'; }); } function regTokensList() { const homeserver = _val('homeserver'); const access_token = _val('access_token'); if (!homeserver) { alert('Homeserver URL is required'); return; } if (!access_token) { alert('Access token is required'); return; } fetch(`${homeserver}/_synapse/admin/v1/registration_tokens`, { method: 'GET', headers: { 'Authorization': `Bearer ${access_token}` } }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); return response.json(); }) .then((data) => { let tableHtml = ` <tr> <th>Token</th> <th>Uses Allowed</th> <th>Pending</th> <th>Completed</th> <th>Expires On</th> <th></th> </tr>`; for (let i = 0; i < data.registration_tokens.length; i++) { let regToken = data.registration_tokens[i]; tableHtml += ` <tr> <td>${regToken.token}</td> <td>${regToken.uses_allowed ? regToken.uses_allowed : 'Infinite'}</td> <td>${regToken.pending}</td> <td>${regToken.completed}</td> <td>${regToken.expiry_time ? new Date(regToken.expiry_time) : 'Never'}</td> <td><button onclick='regTokenDelete(${i},"${regToken.token}")'>Delete</button></td> </tr>`; }; _el('reg_tokens_table').innerHTML = tableHtml; }) .catch((errorText) => { alert(`Error getting a list of registration tokens: ${errorText}`); }); } function regTokenDelete(i, token) { const homeserver = _val('homeserver'); const access_token = _val('access_token'); fetch(`${homeserver}/_synapse/admin/v1/registration_tokens/${token}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' } }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); alert(`Registration token ${token} successfully deleted.`) }) .catch((errorText) => { alert(`Error deleting registration token ${token}: ${errorText}`); }); } function regTokenNew() { const homeserver = _val('homeserver'); const access_token = _val('access_token'); if (!homeserver) { alert('Homeserver URL is required'); return; } if (!access_token) { alert('Access token is required'); return; } let body = {}; const token = _val('reg_token'); if (token) { body.token = token; } const uses_allowed = _val('reg_token_uses_allowed'); if (uses_allowed) { body.uses_allowed = parseInt(uses_allowed); } const expiry_time = _val('reg_token_expiry_time'); if (expiry_time) { body.expiry_time = Date.parse(expiry_time); } fetch(`${homeserver}/_synapse/admin/v1/registration_tokens/new`, { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .then((response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); alert('Registration token successfully created.') }) .catch((errorText) => { alert(`Error creating new registration token: ${errorText}.`); }); } </script> <style> html { font-family: Verdana,sans-serif; background-color: #aaa; } div, h1, h2, h3 { margin-top: 2px; margin-bottom: 2px; } button, input { padding: 4px; border-radius: 4px; } table { border-collapse: collapse; border: 1px solid; } th, td { border: 1px solid; padding: 4px; white-space: nowrap; } </style> </head> <body> <h1>Simple Synapse Admin</h1> <div> <small>by <a href='https://yaky.dev' target='_blank'>yaky.dev</a> | <a href='https://github.com/yaky-dev/simple-synapse-admin#readme' target='_blank'>readme</a> | <a href='https://github.com/yaky-dev/simple-synapse-admin' target='_blank'>source</a></small> </div> <div> Homeserver URL: <input id='homeserver' type='text' value=''> </div> <div> Access token (<a href='https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html#authenticate-as-a-server-admin' target='_blank'>how to</a>): <input id='access_token' type='password' value=''> </div> <div> <button onclick='credentialsSave()'>Save credentials</button> (credentials stay in local storage in the browser on your machine) </div> <div> <button onclick='credentialsClear()'>Clear credentials</button> </div> <hr> <h2>Users</h2> <div> <button onclick='usersList(0)'>List users</button> </div> <div> <span id='users_total'></span> </div> <div> <table id='users_table'></table> </div> <div> <button id='users_next' onclick='usersList(0)' disabled='true'>Next</button> </div> <hr> <h2>Rooms</h2> <div> Search: <input id='search_term' type='text'> <button onclick='roomsList(0)'>List rooms</button> </div> <div> <span id='rooms_total'></span> </div> <div> <table id='rooms_table'></table> </div> <div> <button id='rooms_prev' onclick='roomsList(0)' disabled='true'>Prev</button> <button id='rooms_next' onclick='roomsList(0)' disabled='true'>Next</button> </div> <hr> <h2>Registration Tokens</h2> <div> <button onclick='regTokensList()'>List tokens</button> </div> <div> <table id='reg_tokens_table'></table> </div> <h3>New Registration Token</h3> <div> Token (leave blank for randomly generated): <input id='reg_token' type='text'> </div> <div> Uses (leave blank for unlimited uses): <input id='reg_token_uses_allowed' type='number'> </div> <div> Expiration (leave blank for no expiration)<input id='reg_token_expiry_time' type='date'> </div> <div> <button onclick='regTokenNew()'>Create new token</button> </div> </body> </html>