mirror of
https://github.com/etkecc/synapse-admin.git
synced 2024-11-21 23:35:22 +03:00
Add option for access token login (#58)
* Fix SSO login flow, redirect is done after auth * Add accessToken login * Add confirmation for session destroy on accessToken logout * add translations, fix tests, minor renaming * update readme
This commit is contained in:
parent
19302466ef
commit
0d021021df
15 changed files with 709 additions and 542 deletions
|
@ -65,6 +65,7 @@ The following changes are already implemented:
|
|||
* [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
|
||||
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
|
||||
* [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
|
||||
* [Login with access token](https://github.com/etkecc/synapse-admin/pull/58)
|
||||
|
||||
_the list will be updated as new changes are added_
|
||||
|
||||
|
|
|
@ -1,26 +1,70 @@
|
|||
import { Layout, Menu } from 'react-admin';
|
||||
import LiveHelpIcon from '@mui/icons-material/LiveHelp';
|
||||
import { AppBar, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
|
||||
import LiveHelpIcon from "@mui/icons-material/LiveHelp";
|
||||
import { LoginMethod } from "../pages/LoginPage";
|
||||
import { useState } from "react";
|
||||
|
||||
const DEFAULT_SUPPORT_LINK = "https://github.com/etkecc/synapse-admin/issues";
|
||||
const supportLink = (): string => {
|
||||
try {
|
||||
new URL(localStorage.getItem("support_url") || ''); // Check if the URL is valid
|
||||
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
|
||||
} catch (e) {
|
||||
return DEFAULT_SUPPORT_LINK;
|
||||
}
|
||||
try {
|
||||
new URL(localStorage.getItem("support_url") || ""); // Check if the URL is valid
|
||||
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
|
||||
} catch (e) {
|
||||
return DEFAULT_SUPPORT_LINK;
|
||||
}
|
||||
};
|
||||
|
||||
const AdminUserMenu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const logout = useLogout();
|
||||
const checkLoginType = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
const loginType: LoginMethod = (localStorage.getItem("login_type") || "credentials") as LoginMethod;
|
||||
if (loginType === "accessToken") {
|
||||
ev.stopPropagation();
|
||||
setOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setOpen(false);
|
||||
logout();
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setOpen(false);
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("login_type");
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<UserMenu>
|
||||
<div onClickCapture={checkLoginType}>
|
||||
<Logout />
|
||||
</div>
|
||||
<Confirm
|
||||
isOpen={open}
|
||||
title="synapseadmin.auth.logout_acces_token_dialog.title"
|
||||
content="synapseadmin.auth.logout_acces_token_dialog.content"
|
||||
onConfirm={handleConfirm}
|
||||
onClose={handleDialogClose}
|
||||
confirm="synapseadmin.auth.logout_acces_token_dialog.confirm"
|
||||
cancel="synapseadmin.auth.logout_acces_token_dialog.cancel"
|
||||
/>
|
||||
</UserMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminAppBar = () => <AppBar userMenu={<AdminUserMenu />} />;
|
||||
|
||||
const AdminMenu = () => (
|
||||
<Menu>
|
||||
<Menu.ResourceItems />
|
||||
<Menu.Item to={supportLink()} target="_blank" primaryText="Contact support" leftIcon={<LiveHelpIcon />} />
|
||||
</Menu>
|
||||
<Menu>
|
||||
<Menu.ResourceItems />
|
||||
<Menu.Item to={supportLink()} target="_blank" primaryText="Contact support" leftIcon={<LiveHelpIcon />} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
export const AdminLayout = ({ children }) => (
|
||||
<Layout menu={AdminMenu}>
|
||||
{children}
|
||||
</Layout>
|
||||
<Layout appBar={AdminAppBar} menu={AdminMenu}>
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
|
|
58
src/components/LoginFormBox.tsx
Normal file
58
src/components/LoginFormBox.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { styled } from "@mui/material/styles";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
const LoginFormBox = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "calc(100vh - 1rem)",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
background: "url(./images/floating-cogs.svg)",
|
||||
backgroundColor: "#f9f9f9",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover",
|
||||
|
||||
[`& .card`]: {
|
||||
width: "30rem",
|
||||
marginTop: "6rem",
|
||||
marginBottom: "6rem",
|
||||
},
|
||||
[`& .avatar`]: {
|
||||
margin: "1rem",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
[`& .icon`]: {
|
||||
backgroundColor: theme.palette.grey[500],
|
||||
},
|
||||
[`& .hint`]: {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
color: theme.palette.grey[600],
|
||||
},
|
||||
[`& .form`]: {
|
||||
padding: "0 1rem 1rem 1rem",
|
||||
},
|
||||
[`& .select`]: {
|
||||
marginBottom: "2rem",
|
||||
},
|
||||
[`& .actions`]: {
|
||||
padding: "0 1rem 1rem 1rem",
|
||||
},
|
||||
[`& .serverVersion`]: {
|
||||
color: theme.palette.grey[500],
|
||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||
marginLeft: "0.5rem",
|
||||
},
|
||||
[`& .matrixVersions`]: {
|
||||
color: theme.palette.grey[500],
|
||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||
fontSize: "0.8rem",
|
||||
marginBottom: "1rem",
|
||||
marginLeft: "0.5rem",
|
||||
},
|
||||
}));
|
||||
|
||||
export default LoginFormBox;
|
|
@ -22,6 +22,14 @@ const de: SynapseTranslationMessages = {
|
|||
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
|
||||
url_error: "Keine gültige Matrix Server URL",
|
||||
sso_sign_in: "Anmeldung mit SSO",
|
||||
credentials: "Anmeldedaten",
|
||||
access_token: "Zugriffstoken",
|
||||
logout_acces_token_dialog: {
|
||||
title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.",
|
||||
content: "Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?",
|
||||
confirm: "Sitzung beenden",
|
||||
cancel: "Nur vom Admin-Panel abmelden",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
||||
|
|
|
@ -14,6 +14,14 @@ const en: SynapseTranslationMessages = {
|
|||
protocol_error: "URL has to start with 'http://' or 'https://'",
|
||||
url_error: "Not a valid Matrix server URL",
|
||||
sso_sign_in: "Sign in with SSO",
|
||||
credentials: "Credentials",
|
||||
access_token: "Access token",
|
||||
logout_acces_token_dialog: {
|
||||
title: "You are using an existing Matrix access token.",
|
||||
content: "Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?",
|
||||
confirm: "Destroy session",
|
||||
cancel: "Just logout from admin panel",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
||||
|
|
|
@ -13,6 +13,14 @@ const fa: SynapseTranslationMessages = {
|
|||
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
|
||||
url_error: "آدرس وارد شده یک سرور معتبر نیست",
|
||||
sso_sign_in: "با SSO وارد شوید",
|
||||
credentials: "اعتبارنامه",
|
||||
access_token: "توکن دسترسی",
|
||||
logout_acces_token_dialog: {
|
||||
title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.",
|
||||
content: "آیا میخواهید این جلسه (که میتواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟",
|
||||
confirm: "نابودی جلسه",
|
||||
cancel: "فقط خروج از پنل مدیریت",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
||||
|
|
|
@ -13,6 +13,14 @@ const fr: SynapseTranslationMessages = {
|
|||
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
|
||||
url_error: "L'URL du serveur Matrix n'est pas valide",
|
||||
sso_sign_in: "Se connecter avec l’authentification unique",
|
||||
credentials: "Identifiants",
|
||||
access_token: "Jeton d'accès",
|
||||
logout_acces_token_dialog: {
|
||||
title: "Vous utilisez un jeton d'accès Matrix existant.",
|
||||
content: "Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?",
|
||||
confirm: "Détruire la session",
|
||||
cancel: "Se déconnecter simplement du panneau d'administration",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
||||
|
@ -201,9 +209,9 @@ const fr: SynapseTranslationMessages = {
|
|||
title: "Supprimer le salon",
|
||||
content:
|
||||
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
|
||||
fields: {
|
||||
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
|
||||
},
|
||||
fields: {
|
||||
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
|
||||
},
|
||||
success: "Salle/s supprimées avec succès.",
|
||||
failure: "La/les salle/s n'ont pas pu être supprimées.",
|
||||
},
|
||||
|
|
8
src/i18n/index.d.ts
vendored
8
src/i18n/index.d.ts
vendored
|
@ -11,6 +11,14 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||
protocol_error: string;
|
||||
url_error: string;
|
||||
sso_sign_in: string;
|
||||
credentials: string;
|
||||
access_token: string;
|
||||
logout_acces_token_dialog: {
|
||||
title: string;
|
||||
content: string;
|
||||
confirm: string;
|
||||
cancel: string;
|
||||
};
|
||||
};
|
||||
users: {
|
||||
invalid_user_id: string;
|
||||
|
|
396
src/i18n/it.ts
396
src/i18n/it.ts
|
@ -13,6 +13,14 @@ const it: SynapseTranslationMessages = {
|
|||
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
||||
url_error: "URL del server Matrix non valido",
|
||||
sso_sign_in: "Accedi con SSO",
|
||||
credentials: "Credenziali",
|
||||
access_token: "Token di accesso",
|
||||
logout_acces_token_dialog: {
|
||||
title: "Stai utilizzando un token di accesso Matrix esistente.",
|
||||
content: "Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?",
|
||||
confirm: "Distruggi sessione",
|
||||
cancel: "Disconnetti solo dal pannello di amministrazione",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
invalid_user_id: "ID utente non valido su questo homeserver.",
|
||||
|
@ -174,211 +182,211 @@ const it: SynapseTranslationMessages = {
|
|||
},
|
||||
helper: {
|
||||
/* forward_extremities:
|
||||
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
|
||||
},
|
||||
enums: {
|
||||
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
|
||||
},
|
||||
enums: {
|
||||
join_rules: {
|
||||
public: "Pubblica",
|
||||
knock: "Bussa",
|
||||
invite: "Invita",
|
||||
private: "Privata",
|
||||
},
|
||||
guest_access: {
|
||||
can_join: "Gli utenti ospiti possono entrare",
|
||||
forbidden: "Gli utenti ospiti non possono entrare",
|
||||
},
|
||||
history_visibility: {
|
||||
invited: "Dall'invito",
|
||||
joined: "Dall'entrata",
|
||||
shared: "Dalla condivisione",
|
||||
world_readable: "Chiunque",
|
||||
},
|
||||
unencrypted: "Non criptata",
|
||||
public: "Pubblica",
|
||||
knock: "Bussa",
|
||||
invite: "Invita",
|
||||
private: "Privata",
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Cancella stanza",
|
||||
content:
|
||||
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
|
||||
},
|
||||
guest_access: {
|
||||
can_join: "Gli utenti ospiti possono entrare",
|
||||
forbidden: "Gli utenti ospiti non possono entrare",
|
||||
},
|
||||
history_visibility: {
|
||||
invited: "Dall'invito",
|
||||
joined: "Dall'entrata",
|
||||
shared: "Dalla condivisione",
|
||||
world_readable: "Chiunque",
|
||||
},
|
||||
unencrypted: "Non criptata",
|
||||
},
|
||||
reports: {
|
||||
name: "Evento segnalato |||| Eventi segnalati",
|
||||
fields: {
|
||||
id: "ID",
|
||||
received_ts: "Orario del report",
|
||||
user_id: "richiedente",
|
||||
name: "nome della stanza",
|
||||
score: "punteggio",
|
||||
reason: "ragione",
|
||||
event_id: "ID dell'evento",
|
||||
event_json: {
|
||||
origin: "server di origine",
|
||||
origin_server_ts: "ora dell'invio",
|
||||
type: "tipo di evento",
|
||||
content: {
|
||||
msgtype: "tipo di contenuto",
|
||||
body: "contenuto",
|
||||
format: "formato",
|
||||
formatted_body: "contenuto formattato",
|
||||
algorithm: "algoritmo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
name: "Connessioni",
|
||||
fields: {
|
||||
last_seen: "Data",
|
||||
ip: "Indirizzo IP",
|
||||
user_agent: "agente utente",
|
||||
},
|
||||
},
|
||||
devices: {
|
||||
name: "Dispositivo |||| Dispositivi",
|
||||
fields: {
|
||||
device_id: "ID del dispositivo",
|
||||
display_name: "Nome del dispositivo",
|
||||
last_seen_ts: "Timestamp",
|
||||
last_seen_ip: "Indirizzo IP",
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Rimozione del dispositivo %{id}",
|
||||
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
|
||||
success: "Dispositivo rimosso con successo.",
|
||||
failure: "C'è stato un errore.",
|
||||
},
|
||||
},
|
||||
},
|
||||
users_media: {
|
||||
name: "Media",
|
||||
fields: {
|
||||
media_id: "ID del media",
|
||||
media_length: "Peso del file (in Byte)",
|
||||
media_type: "Tipo",
|
||||
upload_name: "Nome del file",
|
||||
quarantined_by: "In quarantena da",
|
||||
safe_from_quarantine: "Protetto dalla quarantena",
|
||||
created_ts: "Creato",
|
||||
last_access_ts: "Ultimo accesso",
|
||||
},
|
||||
},
|
||||
protect_media: {
|
||||
action: {
|
||||
create: "Non protetto, proteggi",
|
||||
delete: "Protetto, rimuovi protezione",
|
||||
none: "In quarantena",
|
||||
send_success: "Stato della protezione cambiato con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
},
|
||||
},
|
||||
quarantine_media: {
|
||||
action: {
|
||||
name: "Quarantina",
|
||||
create: "Aggiungi alla quarantena",
|
||||
delete: "In quarantena, rimuovi dalla quarantena",
|
||||
none: "Protetto dalla quarantena",
|
||||
send_success: "Stato della quarantena cambiato con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
},
|
||||
},
|
||||
pushers: {
|
||||
name: "Pusher |||| Pusher",
|
||||
fields: {
|
||||
app: "App",
|
||||
app_display_name: "Nome dell'app",
|
||||
app_id: "ID dell'app",
|
||||
device_display_name: "Nome del dispositivo",
|
||||
kind: "Tipo",
|
||||
lang: "Lingua",
|
||||
profile_tag: "Tag del profilo",
|
||||
pushkey: "Pushkey",
|
||||
data: { url: "URL" },
|
||||
},
|
||||
},
|
||||
servernotices: {
|
||||
name: "Avvisi del server",
|
||||
send: "Invia avvisi",
|
||||
fields: {
|
||||
body: "Messaggio",
|
||||
},
|
||||
action: {
|
||||
send: "Invia nota",
|
||||
send_success: "Avviso inviato con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
},
|
||||
helper: {
|
||||
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
name: "Media degli utenti",
|
||||
fields: {
|
||||
media_count: "Numero media",
|
||||
media_length: "Lunghezza media",
|
||||
},
|
||||
},
|
||||
forward_extremities: {
|
||||
name: "Invia estremità",
|
||||
fields: {
|
||||
id: "Event ID",
|
||||
received_ts: "Timestamp",
|
||||
depth: "Profondità",
|
||||
state_group: "State group",
|
||||
},
|
||||
},
|
||||
room_state: {
|
||||
name: "Eventi di stato",
|
||||
fields: {
|
||||
type: "Tipo",
|
||||
content: "Contenuto",
|
||||
origin_server_ts: "Ora dell'invio",
|
||||
sender: "Mittente",
|
||||
},
|
||||
},
|
||||
room_directory: {
|
||||
name: "Elenco delle stanze",
|
||||
fields: {
|
||||
world_readable: "gli utenti ospite possono vedere senza entrare",
|
||||
guest_can_join: "gli utenti ospite possono entrare",
|
||||
},
|
||||
action: {
|
||||
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
|
||||
action: {
|
||||
erase: {
|
||||
title: "Cancella stanza",
|
||||
content:
|
||||
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
|
||||
erase: "Rimuovi dall'elenco",
|
||||
create: "Crea",
|
||||
send_success: "Stanza creata con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
|
||||
},
|
||||
},
|
||||
destinations: {
|
||||
name: "Federazione",
|
||||
fields: {
|
||||
destination: "Destinazione",
|
||||
failure_ts: "Timestamp dell'errore",
|
||||
retry_last_ts: "Tentativo ultimo timestamp",
|
||||
retry_interval: "Intervallo dei tentativi",
|
||||
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
|
||||
stream_ordering: "Flusso",
|
||||
},
|
||||
reports: {
|
||||
name: "Evento segnalato |||| Eventi segnalati",
|
||||
fields: {
|
||||
id: "ID",
|
||||
received_ts: "Orario del report",
|
||||
user_id: "richiedente",
|
||||
name: "nome della stanza",
|
||||
score: "punteggio",
|
||||
reason: "ragione",
|
||||
event_id: "ID dell'evento",
|
||||
event_json: {
|
||||
origin: "server di origine",
|
||||
origin_server_ts: "ora dell'invio",
|
||||
type: "tipo di evento",
|
||||
content: {
|
||||
msgtype: "tipo di contenuto",
|
||||
body: "contenuto",
|
||||
format: "formato",
|
||||
formatted_body: "contenuto formattato",
|
||||
algorithm: "algoritmo",
|
||||
},
|
||||
},
|
||||
action: { reconnect: "Riconnetti" },
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Token di registrazione",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Token valido",
|
||||
uses_allowed: "Usi permessi",
|
||||
pending: "In attesa",
|
||||
completed: "Completato",
|
||||
expiry_time: "Data della scadenza",
|
||||
length: "Lunghezza",
|
||||
},
|
||||
connections: {
|
||||
name: "Connessioni",
|
||||
fields: {
|
||||
last_seen: "Data",
|
||||
ip: "Indirizzo IP",
|
||||
user_agent: "agente utente",
|
||||
},
|
||||
},
|
||||
devices: {
|
||||
name: "Dispositivo |||| Dispositivi",
|
||||
fields: {
|
||||
device_id: "ID del dispositivo",
|
||||
display_name: "Nome del dispositivo",
|
||||
last_seen_ts: "Timestamp",
|
||||
last_seen_ip: "Indirizzo IP",
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Rimozione del dispositivo %{id}",
|
||||
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
|
||||
success: "Dispositivo rimosso con successo.",
|
||||
failure: "C'è stato un errore.",
|
||||
},
|
||||
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
||||
},
|
||||
},
|
||||
users_media: {
|
||||
name: "Media",
|
||||
fields: {
|
||||
media_id: "ID del media",
|
||||
media_length: "Peso del file (in Byte)",
|
||||
media_type: "Tipo",
|
||||
upload_name: "Nome del file",
|
||||
quarantined_by: "In quarantena da",
|
||||
safe_from_quarantine: "Protetto dalla quarantena",
|
||||
created_ts: "Creato",
|
||||
last_access_ts: "Ultimo accesso",
|
||||
},
|
||||
},
|
||||
protect_media: {
|
||||
action: {
|
||||
create: "Non protetto, proteggi",
|
||||
delete: "Protetto, rimuovi protezione",
|
||||
none: "In quarantena",
|
||||
send_success: "Stato della protezione cambiato con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
},
|
||||
},
|
||||
quarantine_media: {
|
||||
action: {
|
||||
name: "Quarantina",
|
||||
create: "Aggiungi alla quarantena",
|
||||
delete: "In quarantena, rimuovi dalla quarantena",
|
||||
none: "Protetto dalla quarantena",
|
||||
send_success: "Stato della quarantena cambiato con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
},
|
||||
},
|
||||
pushers: {
|
||||
name: "Pusher |||| Pusher",
|
||||
fields: {
|
||||
app: "App",
|
||||
app_display_name: "Nome dell'app",
|
||||
app_id: "ID dell'app",
|
||||
device_display_name: "Nome del dispositivo",
|
||||
kind: "Tipo",
|
||||
lang: "Lingua",
|
||||
profile_tag: "Tag del profilo",
|
||||
pushkey: "Pushkey",
|
||||
data: { url: "URL" },
|
||||
},
|
||||
},
|
||||
servernotices: {
|
||||
name: "Avvisi del server",
|
||||
send: "Invia avvisi",
|
||||
fields: {
|
||||
body: "Messaggio",
|
||||
},
|
||||
action: {
|
||||
send: "Invia nota",
|
||||
send_success: "Avviso inviato con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
},
|
||||
helper: {
|
||||
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
name: "Media degli utenti",
|
||||
fields: {
|
||||
media_count: "Numero media",
|
||||
media_length: "Lunghezza media",
|
||||
},
|
||||
},
|
||||
forward_extremities: {
|
||||
name: "Invia estremità",
|
||||
fields: {
|
||||
id: "Event ID",
|
||||
received_ts: "Timestamp",
|
||||
depth: "Profondità",
|
||||
state_group: "State group",
|
||||
},
|
||||
},
|
||||
room_state: {
|
||||
name: "Eventi di stato",
|
||||
fields: {
|
||||
type: "Tipo",
|
||||
content: "Contenuto",
|
||||
origin_server_ts: "Ora dell'invio",
|
||||
sender: "Mittente",
|
||||
},
|
||||
},
|
||||
room_directory: {
|
||||
name: "Elenco delle stanze",
|
||||
fields: {
|
||||
world_readable: "gli utenti ospite possono vedere senza entrare",
|
||||
guest_can_join: "gli utenti ospite possono entrare",
|
||||
},
|
||||
action: {
|
||||
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
|
||||
content:
|
||||
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
|
||||
erase: "Rimuovi dall'elenco",
|
||||
create: "Crea",
|
||||
send_success: "Stanza creata con successo.",
|
||||
send_failure: "C'è stato un errore.",
|
||||
},
|
||||
},
|
||||
destinations: {
|
||||
name: "Federazione",
|
||||
fields: {
|
||||
destination: "Destinazione",
|
||||
failure_ts: "Timestamp dell'errore",
|
||||
retry_last_ts: "Tentativo ultimo timestamp",
|
||||
retry_interval: "Intervallo dei tentativi",
|
||||
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
|
||||
stream_ordering: "Flusso",
|
||||
},
|
||||
action: { reconnect: "Riconnetti" },
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Token di registrazione",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Token valido",
|
||||
uses_allowed: "Usi permessi",
|
||||
pending: "In attesa",
|
||||
completed: "Completato",
|
||||
expiry_time: "Data della scadenza",
|
||||
length: "Lunghezza",
|
||||
},
|
||||
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
||||
},
|
||||
},
|
||||
};
|
||||
export default it;
|
||||
|
|
470
src/i18n/ru.ts
470
src/i18n/ru.ts
|
@ -22,6 +22,14 @@ const ru: SynapseTranslationMessages = {
|
|||
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
|
||||
url_error: "Неверный адрес сервера Matrix",
|
||||
sso_sign_in: "Вход через SSO",
|
||||
credentials: "Учетные данные",
|
||||
access_token: "Токен доступа",
|
||||
logout_acces_token_dialog: {
|
||||
title: "Вы используете существующий токен доступа Matrix.",
|
||||
content: "Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?",
|
||||
confirm: "Завершить сессию",
|
||||
cancel: "Просто выйти из панели администрирования",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
||||
|
@ -87,7 +95,7 @@ const ru: SynapseTranslationMessages = {
|
|||
header: "Загрузить CSV файл",
|
||||
explanation:
|
||||
"Здесь вы можете загрузить файл со значениями, разделёнными запятыми, которые будут использованы для создания или обновления данных пользователей. \
|
||||
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
|
||||
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
|
||||
},
|
||||
startImport: {
|
||||
simulate_only: "Только симулировать",
|
||||
|
@ -122,10 +130,10 @@ const ru: SynapseTranslationMessages = {
|
|||
helper: {
|
||||
send: "Это API удаляет локальные файлы с вашего собственного сервера, включая локальные миниатюры и копии скачанных файлов. \
|
||||
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
||||
},
|
||||
},
|
||||
resources: {
|
||||
users: {
|
||||
},
|
||||
},
|
||||
resources: {
|
||||
users: {
|
||||
name: "Пользователь |||| Пользователи",
|
||||
email: "Почта",
|
||||
msisdn: "Телефон",
|
||||
|
@ -169,258 +177,258 @@ const ru: SynapseTranslationMessages = {
|
|||
delete_media: "Удаление всех медиафайлов, загруженных пользователем (-ами)",
|
||||
redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
name: "Комната |||| Комнаты",
|
||||
fields: {
|
||||
room_id: "ID комнаты",
|
||||
name: "Название",
|
||||
canonical_alias: "Псевдоним",
|
||||
joined_members: "Участники",
|
||||
joined_local_members: "Локальные участники",
|
||||
joined_local_devices: "Локальные устройства",
|
||||
state_events: "События состояния / Сложность",
|
||||
version: "Версия",
|
||||
is_encrypted: "Зашифровано",
|
||||
encryption: "Шифрование",
|
||||
federatable: "Федерация",
|
||||
public: "Отображается в каталоге комнат",
|
||||
creator: "Создатель",
|
||||
join_rules: "Правила входа",
|
||||
guest_access: "Гостевой доступ",
|
||||
history_visibility: "Видимость истории",
|
||||
topic: "Тема",
|
||||
avatar: "Аватар",
|
||||
},
|
||||
helper: {
|
||||
forward_extremities:
|
||||
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
|
||||
rooms: {
|
||||
name: "Комната |||| Комнаты",
|
||||
fields: {
|
||||
room_id: "ID комнаты",
|
||||
name: "Название",
|
||||
canonical_alias: "Псевдоним",
|
||||
joined_members: "Участники",
|
||||
joined_local_members: "Локальные участники",
|
||||
joined_local_devices: "Локальные устройства",
|
||||
state_events: "События состояния / Сложность",
|
||||
version: "Версия",
|
||||
is_encrypted: "Зашифровано",
|
||||
encryption: "Шифрование",
|
||||
federatable: "Федерация",
|
||||
public: "Отображается в каталоге комнат",
|
||||
creator: "Создатель",
|
||||
join_rules: "Правила входа",
|
||||
guest_access: "Гостевой доступ",
|
||||
history_visibility: "Видимость истории",
|
||||
topic: "Тема",
|
||||
avatar: "Аватар",
|
||||
},
|
||||
helper: {
|
||||
forward_extremities:
|
||||
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
|
||||
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
|
||||
Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
|
||||
Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
|
||||
},
|
||||
enums: {
|
||||
join_rules: {
|
||||
public: "Для всех",
|
||||
knock: "Надо постучать",
|
||||
invite: "По приглашению",
|
||||
private: "Приватная",
|
||||
},
|
||||
guest_access: {
|
||||
can_join: "Гости могут войти",
|
||||
forbidden: "Гости не могут войти",
|
||||
},
|
||||
history_visibility: {
|
||||
invited: "С момента приглашения",
|
||||
joined: "С момента входа",
|
||||
shared: "С момента открытия доступа",
|
||||
world_readable: "Для всех",
|
||||
},
|
||||
unencrypted: "Без шифрования",
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Удалить комнату",
|
||||
content:
|
||||
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
|
||||
fields: {
|
||||
block: "Заблокировать и запретить пользователям присоединяться к комнате",
|
||||
enums: {
|
||||
join_rules: {
|
||||
public: "Для всех",
|
||||
knock: "Надо постучать",
|
||||
invite: "По приглашению",
|
||||
private: "Приватная",
|
||||
},
|
||||
guest_access: {
|
||||
can_join: "Гости могут войти",
|
||||
forbidden: "Гости не могут войти",
|
||||
},
|
||||
history_visibility: {
|
||||
invited: "С момента приглашения",
|
||||
joined: "С момента входа",
|
||||
shared: "С момента открытия доступа",
|
||||
world_readable: "Для всех",
|
||||
},
|
||||
unencrypted: "Без шифрования",
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Удалить комнату",
|
||||
content:
|
||||
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
|
||||
fields: {
|
||||
block: "Заблокировать и запретить пользователям присоединяться к комнате",
|
||||
},
|
||||
success: "Комната/ы успешно удалены",
|
||||
failure: "Комната/ы не могут быть удалены.",
|
||||
},
|
||||
success: "Комната/ы успешно удалены",
|
||||
failure: "Комната/ы не могут быть удалены.",
|
||||
},
|
||||
},
|
||||
},
|
||||
reports: {
|
||||
name: "Жалоба |||| Жалобы",
|
||||
fields: {
|
||||
id: "ID",
|
||||
received_ts: "Дата и время жалобы",
|
||||
user_id: "Автор жалобы",
|
||||
name: "Название комнаты",
|
||||
score: "Баллы",
|
||||
reason: "Причина",
|
||||
event_id: "ID события",
|
||||
event_json: {
|
||||
origin: "Исходнный сервер",
|
||||
origin_server_ts: "Дата и время отправки",
|
||||
type: "Тип события",
|
||||
content: {
|
||||
msgtype: "Тип содержимого",
|
||||
body: "Содержимое",
|
||||
format: "Формат",
|
||||
formatted_body: "Форматированное содержимое",
|
||||
algorithm: "Алгоритм",
|
||||
url: "Ссылка",
|
||||
info: {
|
||||
mimetype: "Тип",
|
||||
reports: {
|
||||
name: "Жалоба |||| Жалобы",
|
||||
fields: {
|
||||
id: "ID",
|
||||
received_ts: "Дата и время жалобы",
|
||||
user_id: "Автор жалобы",
|
||||
name: "Название комнаты",
|
||||
score: "Баллы",
|
||||
reason: "Причина",
|
||||
event_id: "ID события",
|
||||
event_json: {
|
||||
origin: "Исходнный сервер",
|
||||
origin_server_ts: "Дата и время отправки",
|
||||
type: "Тип события",
|
||||
content: {
|
||||
msgtype: "Тип содержимого",
|
||||
body: "Содержимое",
|
||||
format: "Формат",
|
||||
formatted_body: "Форматированное содержимое",
|
||||
algorithm: "Алгоритм",
|
||||
url: "Ссылка",
|
||||
info: {
|
||||
mimetype: "Тип",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Удалить жалобу",
|
||||
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
|
||||
action: {
|
||||
erase: {
|
||||
title: "Удалить жалобу",
|
||||
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
name: "Подключения",
|
||||
fields: {
|
||||
last_seen: "Дата",
|
||||
ip: "IP адрес",
|
||||
user_agent: "Юзер-агент",
|
||||
},
|
||||
},
|
||||
devices: {
|
||||
name: "Устройство |||| Устройства",
|
||||
fields: {
|
||||
device_id: "ID устройства",
|
||||
display_name: "Название",
|
||||
last_seen_ts: "Дата и время",
|
||||
last_seen_ip: "IP адрес",
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Удаление %{id}",
|
||||
content: 'Действительно удалить устройство "%{name}"?',
|
||||
success: "Устройство успешно удалено.",
|
||||
failure: "Произошла ошибка.",
|
||||
connections: {
|
||||
name: "Подключения",
|
||||
fields: {
|
||||
last_seen: "Дата",
|
||||
ip: "IP адрес",
|
||||
user_agent: "Юзер-агент",
|
||||
},
|
||||
},
|
||||
},
|
||||
users_media: {
|
||||
name: "Файлы",
|
||||
fields: {
|
||||
media_id: "ID файла",
|
||||
media_length: "Размер файла (в байтах)",
|
||||
media_type: "Тип",
|
||||
upload_name: "Имя файла",
|
||||
quarantined_by: "На карантине",
|
||||
safe_from_quarantine: "Защитить от карантина",
|
||||
created_ts: "Создано",
|
||||
last_access_ts: "Последний доступ",
|
||||
devices: {
|
||||
name: "Устройство |||| Устройства",
|
||||
fields: {
|
||||
device_id: "ID устройства",
|
||||
display_name: "Название",
|
||||
last_seen_ts: "Дата и время",
|
||||
last_seen_ip: "IP адрес",
|
||||
},
|
||||
action: {
|
||||
erase: {
|
||||
title: "Удаление %{id}",
|
||||
content: 'Действительно удалить устройство "%{name}"?',
|
||||
success: "Устройство успешно удалено.",
|
||||
failure: "Произошла ошибка.",
|
||||
},
|
||||
},
|
||||
},
|
||||
action: {
|
||||
open: "Открыть файл в новом окне",
|
||||
users_media: {
|
||||
name: "Файлы",
|
||||
fields: {
|
||||
media_id: "ID файла",
|
||||
media_length: "Размер файла (в байтах)",
|
||||
media_type: "Тип",
|
||||
upload_name: "Имя файла",
|
||||
quarantined_by: "На карантине",
|
||||
safe_from_quarantine: "Защитить от карантина",
|
||||
created_ts: "Создано",
|
||||
last_access_ts: "Последний доступ",
|
||||
},
|
||||
action: {
|
||||
open: "Открыть файл в новом окне",
|
||||
},
|
||||
},
|
||||
},
|
||||
protect_media: {
|
||||
action: {
|
||||
create: "Не защищён, установить защиту",
|
||||
delete: "Защищён, снять защиту",
|
||||
none: "На карантине",
|
||||
send_success: "Статус защиты успешно изменён.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
protect_media: {
|
||||
action: {
|
||||
create: "Не защищён, установить защиту",
|
||||
delete: "Защищён, снять защиту",
|
||||
none: "На карантине",
|
||||
send_success: "Статус защиты успешно изменён.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
},
|
||||
},
|
||||
},
|
||||
quarantine_media: {
|
||||
action: {
|
||||
name: "Карантин",
|
||||
create: "Поместить на карантин",
|
||||
delete: "На карантине, снять карантин",
|
||||
none: "Защищено от карантина",
|
||||
send_success: "Статус карантина успешно изменён.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
quarantine_media: {
|
||||
action: {
|
||||
name: "Карантин",
|
||||
create: "Поместить на карантин",
|
||||
delete: "На карантине, снять карантин",
|
||||
none: "Защищено от карантина",
|
||||
send_success: "Статус карантина успешно изменён.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
},
|
||||
},
|
||||
},
|
||||
pushers: {
|
||||
name: "Пушер |||| Пушеры",
|
||||
fields: {
|
||||
app: "Приложение",
|
||||
app_display_name: "Название приложения",
|
||||
app_id: "ID приложения",
|
||||
device_display_name: "Название устройства",
|
||||
kind: "Вид",
|
||||
lang: "Язык",
|
||||
profile_tag: "Тег профиля",
|
||||
pushkey: "Ключ",
|
||||
data: { url: "URL" },
|
||||
pushers: {
|
||||
name: "Пушер |||| Пушеры",
|
||||
fields: {
|
||||
app: "Приложение",
|
||||
app_display_name: "Название приложения",
|
||||
app_id: "ID приложения",
|
||||
device_display_name: "Название устройства",
|
||||
kind: "Вид",
|
||||
lang: "Язык",
|
||||
profile_tag: "Тег профиля",
|
||||
pushkey: "Ключ",
|
||||
data: { url: "URL" },
|
||||
},
|
||||
},
|
||||
},
|
||||
servernotices: {
|
||||
name: "Серверные уведомления",
|
||||
send: "Отправить серверные уведомления",
|
||||
fields: {
|
||||
body: "Сообщение",
|
||||
servernotices: {
|
||||
name: "Серверные уведомления",
|
||||
send: "Отправить серверные уведомления",
|
||||
fields: {
|
||||
body: "Сообщение",
|
||||
},
|
||||
action: {
|
||||
send: "Отправить",
|
||||
send_success: "Серверное уведомление успешно отправлено.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
},
|
||||
helper: {
|
||||
send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
|
||||
},
|
||||
},
|
||||
action: {
|
||||
send: "Отправить",
|
||||
send_success: "Серверное уведомление успешно отправлено.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
user_media_statistics: {
|
||||
name: "Файлы пользователей",
|
||||
fields: {
|
||||
media_count: "Количество файлов",
|
||||
media_length: "Размер файлов",
|
||||
},
|
||||
},
|
||||
helper: {
|
||||
send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
|
||||
forward_extremities: {
|
||||
name: "Оконечности",
|
||||
fields: {
|
||||
id: "ID события",
|
||||
received_ts: "Дата и время",
|
||||
depth: "Глубина",
|
||||
state_group: "Группа состояния",
|
||||
},
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
name: "Файлы пользователей",
|
||||
fields: {
|
||||
media_count: "Количество файлов",
|
||||
media_length: "Размер файлов",
|
||||
room_state: {
|
||||
name: "События состояния",
|
||||
fields: {
|
||||
type: "Тип",
|
||||
content: "Содержимое",
|
||||
origin_server_ts: "Дата отправки",
|
||||
sender: "Отправитель",
|
||||
},
|
||||
},
|
||||
},
|
||||
forward_extremities: {
|
||||
name: "Оконечности",
|
||||
fields: {
|
||||
id: "ID события",
|
||||
received_ts: "Дата и время",
|
||||
depth: "Глубина",
|
||||
state_group: "Группа состояния",
|
||||
room_directory: {
|
||||
name: "Каталог комнат",
|
||||
fields: {
|
||||
world_readable: "Гости могут просматривать без входа",
|
||||
guest_can_join: "Гости могут войти",
|
||||
},
|
||||
action: {
|
||||
title:
|
||||
"Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
|
||||
content:
|
||||
"Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
|
||||
erase: "Удалить из каталога комнат",
|
||||
create: "Опубликовать в каталоге комнат",
|
||||
send_success: "Комната успешно опубликована.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
},
|
||||
},
|
||||
},
|
||||
room_state: {
|
||||
name: "События состояния",
|
||||
fields: {
|
||||
type: "Тип",
|
||||
content: "Содержимое",
|
||||
origin_server_ts: "Дата отправки",
|
||||
sender: "Отправитель",
|
||||
destinations: {
|
||||
name: "Федерация",
|
||||
fields: {
|
||||
destination: "Назначение",
|
||||
failure_ts: "Дата и время ошибки",
|
||||
retry_last_ts: "Дата и время последней попытки",
|
||||
retry_interval: "Интервал между попытками",
|
||||
last_successful_stream_ordering: "Последний успешный поток",
|
||||
stream_ordering: "Поток",
|
||||
},
|
||||
action: { reconnect: "Переподключиться" },
|
||||
},
|
||||
},
|
||||
room_directory: {
|
||||
name: "Каталог комнат",
|
||||
fields: {
|
||||
world_readable: "Гости могут просматривать без входа",
|
||||
guest_can_join: "Гости могут войти",
|
||||
registration_tokens: {
|
||||
name: "Токены регистрации",
|
||||
fields: {
|
||||
token: "Токен",
|
||||
valid: "Рабочий токен",
|
||||
uses_allowed: "Количество использований",
|
||||
pending: "Ожидает",
|
||||
completed: "Завершено",
|
||||
expiry_time: "Дата окончания",
|
||||
length: "Длина",
|
||||
},
|
||||
helper: { length: "Длина токена, если токен не задан." },
|
||||
},
|
||||
action: {
|
||||
title:
|
||||
"Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
|
||||
content:
|
||||
"Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
|
||||
erase: "Удалить из каталога комнат",
|
||||
create: "Опубликовать в каталоге комнат",
|
||||
send_success: "Комната успешно опубликована.",
|
||||
send_failure: "Произошла ошибка.",
|
||||
},
|
||||
},
|
||||
destinations: {
|
||||
name: "Федерация",
|
||||
fields: {
|
||||
destination: "Назначение",
|
||||
failure_ts: "Дата и время ошибки",
|
||||
retry_last_ts: "Дата и время последней попытки",
|
||||
retry_interval: "Интервал между попытками",
|
||||
last_successful_stream_ordering: "Последний успешный поток",
|
||||
stream_ordering: "Поток",
|
||||
},
|
||||
action: { reconnect: "Переподключиться" },
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Токены регистрации",
|
||||
fields: {
|
||||
token: "Токен",
|
||||
valid: "Рабочий токен",
|
||||
uses_allowed: "Количество использований",
|
||||
pending: "Ожидает",
|
||||
completed: "Завершено",
|
||||
expiry_time: "Дата окончания",
|
||||
length: "Длина",
|
||||
},
|
||||
helper: { length: "Длина токена, если токен не задан." },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export default ru;
|
||||
|
|
|
@ -21,6 +21,14 @@ const zh: SynapseTranslationMessages = {
|
|||
protocol_error: "URL 需要以'http://'或'https://'作为起始",
|
||||
url_error: "不是一个有效的 Matrix 服务器地址",
|
||||
sso_sign_in: "使用 SSO 登录",
|
||||
credentials: "凭证",
|
||||
access_token: "访问令牌",
|
||||
logout_acces_token_dialog: {
|
||||
title: "您正在使用现有的 Matrix 访问令牌。",
|
||||
content: "您想销毁此会话(可能在其他地方使用,例如在 Matrix 客户端中)还是仅从管理面板退出?",
|
||||
confirm: "销毁会话",
|
||||
cancel: "仅从管理面板退出",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||
|
|
|
@ -9,8 +9,12 @@ import storage from "./storage";
|
|||
fetch("config.json")
|
||||
.then(res => res.json())
|
||||
.then(props => {
|
||||
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
||||
storage.setItem("support_url", props.supportURL);
|
||||
if (props.asManagedUsers) {
|
||||
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
||||
}
|
||||
if (props.supportURL) {
|
||||
storage.setItem("support_url", props.supportURL);
|
||||
}
|
||||
return createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<AppContext.Provider value={props}>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Typography } from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
|
||||
import {
|
||||
Form,
|
||||
FormDataConsumer,
|
||||
|
@ -17,7 +16,7 @@ import {
|
|||
useLocales,
|
||||
} from "react-admin";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import LoginFormBox from "../components/LoginFormBox";
|
||||
import { useAppContext } from "../AppContext";
|
||||
import {
|
||||
getServerVersion,
|
||||
|
@ -29,66 +28,18 @@ import {
|
|||
} from "../synapse/synapse";
|
||||
import storage from "../storage";
|
||||
|
||||
const FormBox = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "calc(100vh - 1rem)",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
background: "url(./images/floating-cogs.svg)",
|
||||
backgroundColor: "#f9f9f9",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover",
|
||||
|
||||
[`& .card`]: {
|
||||
width: "30rem",
|
||||
marginTop: "6rem",
|
||||
marginBottom: "6rem",
|
||||
},
|
||||
[`& .avatar`]: {
|
||||
margin: "1rem",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
[`& .icon`]: {
|
||||
backgroundColor: theme.palette.grey[500],
|
||||
},
|
||||
[`& .hint`]: {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
color: theme.palette.grey[600],
|
||||
},
|
||||
[`& .form`]: {
|
||||
padding: "0 1rem 1rem 1rem",
|
||||
},
|
||||
[`& .select`]: {
|
||||
marginBottom: "2rem",
|
||||
},
|
||||
[`& .actions`]: {
|
||||
padding: "0 1rem 1rem 1rem",
|
||||
},
|
||||
[`& .serverVersion`]: {
|
||||
color: theme.palette.grey[500],
|
||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||
marginLeft: "0.5rem",
|
||||
},
|
||||
[`& .matrixVersions`]: {
|
||||
color: theme.palette.grey[500],
|
||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||
fontSize: "0.8rem",
|
||||
marginBottom: "1rem",
|
||||
marginLeft: "0.5rem",
|
||||
},
|
||||
}));
|
||||
export type LoginMethod = "credentials" | "accessToken";
|
||||
|
||||
const LoginPage = () => {
|
||||
const login = useLogin();
|
||||
const notify = useNotify();
|
||||
const { restrictBaseUrl } = useAppContext();
|
||||
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
|
||||
const allowMultipleBaseUrls = (Array.isArray(restrictBaseUrl) && restrictBaseUrl.length > 0 && restrictBaseUrl[0] !== "" && restrictBaseUrl[0] !== null);
|
||||
const allowMultipleBaseUrls =
|
||||
Array.isArray(restrictBaseUrl) &&
|
||||
restrictBaseUrl.length > 0 &&
|
||||
restrictBaseUrl[0] !== "" &&
|
||||
restrictBaseUrl[0] !== null;
|
||||
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||
|
@ -98,8 +49,13 @@ const LoginPage = () => {
|
|||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
|
||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
||||
|
||||
useEffect(() => {
|
||||
if (!loginToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginToken) {
|
||||
const ssoToken = loginToken[1];
|
||||
console.log("SSO token is", ssoToken);
|
||||
// Prevent further requests
|
||||
|
@ -127,7 +83,7 @@ const LoginPage = () => {
|
|||
console.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [loginToken]);
|
||||
|
||||
const validateBaseUrl = value => {
|
||||
if (!value.match(/^(http|https):\/\//)) {
|
||||
|
@ -213,29 +169,53 @@ const LoginPage = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<TextInput
|
||||
autoFocus
|
||||
source="username"
|
||||
label="ra.auth.username"
|
||||
autoComplete="username"
|
||||
disabled={loading || !supportPassAuth}
|
||||
onBlur={handleUsernameChange}
|
||||
resettable
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<PasswordInput
|
||||
source="password"
|
||||
label="ra.auth.password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
disabled={loading || !supportPassAuth}
|
||||
resettable
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
<Tabs
|
||||
value={loginMethod}
|
||||
onChange={(_, newValue) => setLoginMethod(newValue as LoginMethod)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
centered
|
||||
>
|
||||
<Tab label={translate("synapseadmin.auth.credentials")} value="credentials" />
|
||||
<Tab label={translate("synapseadmin.auth.access_token")} value="accessToken" />
|
||||
</Tabs>
|
||||
{loginMethod === "credentials" ? (
|
||||
<>
|
||||
<Box>
|
||||
<TextInput
|
||||
autoFocus
|
||||
source="username"
|
||||
label="ra.auth.username"
|
||||
autoComplete="username"
|
||||
disabled={loading || !supportPassAuth}
|
||||
onBlur={handleUsernameChange}
|
||||
resettable
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<PasswordInput
|
||||
source="password"
|
||||
label="ra.auth.password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
disabled={loading || !supportPassAuth}
|
||||
resettable
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box>
|
||||
<TextInput
|
||||
source="accessToken"
|
||||
label="synapseadmin.auth.access_token"
|
||||
disabled={loading}
|
||||
resettable
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<TextInput
|
||||
source="base_url"
|
||||
|
@ -263,7 +243,7 @@ const LoginPage = () => {
|
|||
|
||||
return (
|
||||
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
||||
<FormBox>
|
||||
<LoginFormBox>
|
||||
<Card className="card">
|
||||
<Box className="avatar">
|
||||
{loading ? (
|
||||
|
@ -312,7 +292,7 @@ const LoginPage = () => {
|
|||
</CardActions>
|
||||
</Box>
|
||||
</Card>
|
||||
</FormBox>
|
||||
</LoginFormBox>
|
||||
<Notification />
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -23,13 +23,13 @@ describe("authProvider", () => {
|
|||
})
|
||||
);
|
||||
|
||||
const ret: undefined = await authProvider.login({
|
||||
const ret = await authProvider.login({
|
||||
base_url: "http://example.com",
|
||||
username: "@user:example.com",
|
||||
password: "secret",
|
||||
});
|
||||
|
||||
expect(ret).toBe(undefined);
|
||||
expect(ret).toEqual({redirectTo: "/"});
|
||||
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
||||
headers: new Headers({
|
||||
|
@ -55,12 +55,12 @@ describe("authProvider", () => {
|
|||
})
|
||||
);
|
||||
|
||||
const ret: undefined = await authProvider.login({
|
||||
const ret = await authProvider.login({
|
||||
base_url: "https://example.com/",
|
||||
loginToken: "login_token",
|
||||
});
|
||||
|
||||
expect(ret).toBe(undefined);
|
||||
expect(ret).toEqual({redirectTo: "/"});
|
||||
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||
headers: new Headers({
|
||||
|
|
|
@ -10,14 +10,16 @@ const authProvider: AuthProvider = {
|
|||
username,
|
||||
password,
|
||||
loginToken,
|
||||
accessToken,
|
||||
}: {
|
||||
base_url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
loginToken: string;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
console.log("login ");
|
||||
const options: Options = {
|
||||
let options: Options = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(
|
||||
Object.assign(
|
||||
|
@ -55,11 +57,30 @@ const authProvider: AuthProvider = {
|
|||
storage.setItem("base_url", base_url);
|
||||
|
||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/r0/login");
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
if (accessToken) {
|
||||
// this a login with an already obtained access token, let's just validate it
|
||||
options = {
|
||||
headers: new Headers({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
response = await fetchUtils.fetchJson(login_api_url, options);
|
||||
const json = response.json;
|
||||
storage.setItem("home_server", accessToken ? base_url : json.home_server);
|
||||
storage.setItem("user_id", json.user_id);
|
||||
storage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||
storage.setItem("device_id", json.device_id);
|
||||
storage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
||||
|
||||
return Promise.resolve({redirectTo: "/"});
|
||||
} catch(err) {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
|
@ -71,14 +92,8 @@ const authProvider: AuthProvider = {
|
|||
errMsg,
|
||||
errorStatus,
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
const json = response.json;
|
||||
storage.setItem("home_server", json.home_server);
|
||||
storage.setItem("user_id", json.user_id);
|
||||
storage.setItem("access_token", json.access_token);
|
||||
storage.setItem("device_id", json.device_id);
|
||||
},
|
||||
// called when the user clicks on the logout button
|
||||
logout: async () => {
|
||||
|
@ -102,6 +117,7 @@ const authProvider: AuthProvider = {
|
|||
console.log("Error logging out", err);
|
||||
} finally {
|
||||
storage.removeItem("access_token");
|
||||
storage.removeItem("login_type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue