feat(servers/settings): magic string invites, Navidrome admin share, and add-user validation (#258)

* feat(servers): magic string invites and duplicate-aware labels

Add psysonic1- share payloads (URL, Subsonic credentials, optional server
name). Login and add-server forms decode on input and keep magic field
below standard fields. Navidrome User Management generates strings after
PUT password updates where required. Resolve duplicate display names as
username@host in chrome, settings, and login.

* feat(servers): tighten magic-string import and admin share flow

After a decoded magic string, username is read-only and the password field
shows a fixed-length mask so length is not inferred. Password reveal stays
disabled until saved-server quick connect (login) or reopening the add-
server form. Navidrome admin share UI persists the password via API before
copy and adds a plaintext-handling disclaimer. Locales updated.

* feat(settings): save new Navidrome user and copy magic string

Add a non-admin "Save and get magic string" flow: create the user, assign
libraries when needed, then encode credentials to the clipboard. Reuse
the plaintext-handling disclaimer without the password-update hint meant
for edits. Strings added for all locales.

* fix(settings): tighten Navidrome add-user validation and submit feedback

Require username, display name, and a non-empty password (trimmed) for new
users; gate Save and “save and get magic string” on explicit checks with
toast feedback. Show red borders only after a failed submit, and clear
them when the user fills the fields. When editing, keep an empty password
as “unchanged” and validate identity fields with a dedicated message.
Strings: userMgmtValidationMissingIdentity across locales.
This commit is contained in:
cucadmuh
2026-04-22 01:45:28 +03:00
committed by GitHub
parent b61c168430
commit f6f76723d8
16 changed files with 910 additions and 51 deletions
+16
View File
@@ -351,6 +351,9 @@ export const deTranslation = {
urlRequired: 'Bitte Server-URL eingeben.',
savedServers: 'Gespeicherte Server',
addNew: 'Oder neuen Server hinzufügen',
orMagicString: 'Oder Magic-String',
magicStringPlaceholder: 'Freigabe-String einfügen (psysonic1-…)',
magicStringInvalid: 'Ungültiger oder nicht lesbarer Magic-String.',
},
connection: {
connected: 'Verbunden',
@@ -509,6 +512,19 @@ export const deTranslation = {
userMgmtUpdated: 'Benutzer aktualisiert.',
userMgmtDeleted: 'Benutzer gelöscht.',
userMgmtValidationMissing: 'Benutzername, Anzeigename und Passwort sind erforderlich.',
userMgmtValidationMissingIdentity: 'Benutzername und Anzeigename sind erforderlich.',
userMgmtMagicStringGenerate: 'Magic-String erzeugen',
userMgmtSaveAndMagicString: 'Speichern und Magic-String kopieren',
userMgmtMagicStringPasswordNavHint:
'Navidrome speichert dieses Passwort für den Benutzer. Weicht es vom aktuellen ab, wird das Anmeldepasswort auf dem Server aktualisiert.',
userMgmtMagicStringPlaintextWarning:
'Teilen Sie den Magic-String vorsichtig: Er enthält das Passwort im Klartext (Kodierung ist keine Verschlüsselung). Wer die vollständige Zeichenkette hat, kann sich als dieser Benutzer anmelden.',
userMgmtMagicStringCopied: 'Magic-String in die Zwischenablage kopiert.',
userMgmtMagicStringCopyFailed: 'Kopieren in die Zwischenablage fehlgeschlagen.',
userMgmtMagicStringLoginFailed: 'Passwortprüfung fehlgeschlagen — Anmeldedaten konnten nicht verifiziert werden.',
userMgmtMagicStringModalTitle: 'Magic-String erzeugen',
userMgmtMagicStringModalDesc: 'Subsonic-Passwort für „{{username}}" eingeben — es wird in den kopierten Magic-String übernommen.',
userMgmtMagicStringModalConfirm: 'String kopieren',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Aktivieren, wenn dieser Server das <pluginLink>AudioMuse-AI-Navidrome-Plugin</pluginLink> nutzt. Schaltet Instant Mix pro Titel frei und nutzt ähnliche Künstler vom Server statt Last.fm auf Künstlerseiten.',
+16
View File
@@ -352,6 +352,9 @@ export const enTranslation = {
urlRequired: 'Please enter a server URL.',
savedServers: 'Saved Servers',
addNew: 'Or add a new server',
orMagicString: 'Or magic string',
magicStringPlaceholder: 'Paste a share string (psysonic1-…)',
magicStringInvalid: 'Invalid or unreadable magic string.',
},
connection: {
connected: 'Connected',
@@ -511,6 +514,19 @@ export const enTranslation = {
userMgmtUpdated: 'User updated.',
userMgmtDeleted: 'User deleted.',
userMgmtValidationMissing: 'Username, display name and password are required.',
userMgmtValidationMissingIdentity: 'Username and display name are required.',
userMgmtMagicStringGenerate: 'Generate magic string',
userMgmtSaveAndMagicString: 'Save and get magic string',
userMgmtMagicStringPasswordNavHint:
'Navidrome will save this password for the user. If it differs from the current one, the server will update the login password.',
userMgmtMagicStringPlaintextWarning:
'Share the magic string carefully: it contains an unencrypted password (encoding is not encryption). Anyone with the full string can sign in as this user.',
userMgmtMagicStringCopied: 'Magic string copied to clipboard.',
userMgmtMagicStringCopyFailed: 'Could not copy to clipboard.',
userMgmtMagicStringLoginFailed: 'Password check failed — credentials could not be verified.',
userMgmtMagicStringModalTitle: 'Generate magic string',
userMgmtMagicStringModalDesc: 'Enter the Subsonic password for "{{username}}". It is included in the copied magic string.',
userMgmtMagicStringModalConfirm: 'Copy string',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Turn on if this server has the <pluginLink>AudioMuse-AI Navidrome plugin</pluginLink> configured. Enables Instant Mix from tracks and uses server-side similar artists instead of Last.fm on artist pages.',
+16
View File
@@ -353,6 +353,9 @@ export const esTranslation = {
urlRequired: 'Por favor ingresa una URL de servidor.',
savedServers: 'Servidores Guardados',
addNew: 'O agregar un nuevo servidor',
orMagicString: 'O cadena mágica',
magicStringPlaceholder: 'Pega una cadena de invitación (psysonic1-…)',
magicStringInvalid: 'Cadena mágica no válida o ilegible.',
},
connection: {
connected: 'Conectado',
@@ -502,6 +505,19 @@ export const esTranslation = {
userMgmtUpdated: 'Usuario actualizado.',
userMgmtDeleted: 'Usuario eliminado.',
userMgmtValidationMissing: 'Se requieren nombre de usuario, nombre visible y contraseña.',
userMgmtValidationMissingIdentity: 'Se requieren nombre de usuario y nombre visible.',
userMgmtMagicStringGenerate: 'Generar cadena mágica',
userMgmtSaveAndMagicString: 'Guardar y obtener cadena mágica',
userMgmtMagicStringPasswordNavHint:
'Navidrome guardará esta contraseña para el usuario. Si difiere de la actual, se actualizará la contraseña de acceso en el servidor.',
userMgmtMagicStringPlaintextWarning:
'Comparte la cadena mágica con cuidado: contiene la contraseña sin cifrar (la codificación no es cifrado). Quien tenga la cadena completa puede iniciar sesión como este usuario.',
userMgmtMagicStringCopied: 'Cadena mágica copiada al portapapeles.',
userMgmtMagicStringCopyFailed: 'No se pudo copiar al portapapeles.',
userMgmtMagicStringLoginFailed: 'Falló la comprobación de la contraseña; no se pudieron verificar las credenciales.',
userMgmtMagicStringModalTitle: 'Generar cadena mágica',
userMgmtMagicStringModalDesc: 'Introduce la contraseña Subsonic de «{{username}}». Se incluirá en la cadena mágica copiada.',
userMgmtMagicStringModalConfirm: 'Copiar cadena',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Activa si este servidor tiene el plugin <pluginLink>AudioMuse-AI Navidrome</pluginLink> configurado. Habilita Mezcla Instantánea desde pistas y usa artistas similares del servidor en lugar de Last.fm en páginas de artistas.',
+16
View File
@@ -351,6 +351,9 @@ export const frTranslation = {
urlRequired: 'Veuillez saisir une URL de serveur.',
savedServers: 'Serveurs enregistrés',
addNew: 'Ou ajouter un nouveau serveur',
orMagicString: 'Ou chaîne magique',
magicStringPlaceholder: 'Collez une chaîne de partage (psysonic1-…)',
magicStringInvalid: 'Chaîne magique invalide ou illisible.',
},
connection: {
connected: 'Connecté',
@@ -499,6 +502,19 @@ export const frTranslation = {
userMgmtUpdated: 'Utilisateur mis à jour.',
userMgmtDeleted: 'Utilisateur supprimé.',
userMgmtValidationMissing: 'Nom dutilisateur, nom affiché et mot de passe sont requis.',
userMgmtValidationMissingIdentity: 'Le nom dutilisateur et le nom affiché sont requis.',
userMgmtMagicStringGenerate: 'Générer la chaîne magique',
userMgmtSaveAndMagicString: 'Enregistrer et obtenir la chaîne magique',
userMgmtMagicStringPasswordNavHint:
'Navidrome enregistrera ce mot de passe pour lutilisateur. Sil diffère de lactuel, le mot de passe de connexion sur le serveur sera mis à jour.',
userMgmtMagicStringPlaintextWarning:
'Partagez la chaîne magique avec prudence : elle contient un mot de passe non chiffré (lencodage nest pas du chiffrement). Quiconque possède la chaîne complète peut se connecter en tant que cet utilisateur.',
userMgmtMagicStringCopied: 'Chaîne magique copiée dans le presse-papiers.',
userMgmtMagicStringCopyFailed: 'Impossible de copier dans le presse-papiers.',
userMgmtMagicStringLoginFailed: 'Échec de la vérification du mot de passe — identifiants non confirmés.',
userMgmtMagicStringModalTitle: 'Générer la chaîne magique',
userMgmtMagicStringModalDesc: 'Saisissez le mot de passe Subsonic de « {{username}} ». Il est inclus dans la chaîne magique copiée.',
userMgmtMagicStringModalConfirm: 'Copier la chaîne',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Activez si ce serveur utilise le <pluginLink>plugin Navidrome AudioMuse-AI</pluginLink>. Active le mix instantané depuis un morceau et affiche les artistes similaires côté serveur au lieu de Last.fm sur les pages artiste.',
+16
View File
@@ -351,6 +351,9 @@ export const nbTranslation = {
urlRequired: 'Vennligst skriv inn en tjeneer-URL.',
savedServers: 'Lagrede servere',
addNew: 'Eller legg til en ny tjener',
orMagicString: 'Eller magic string',
magicStringPlaceholder: 'Lim inn en delingsstreng (psysonic1-…)',
magicStringInvalid: 'Ugyldig eller uleselig magic string.',
},
connection: {
connected: 'Tilkoblet',
@@ -499,6 +502,19 @@ export const nbTranslation = {
userMgmtUpdated: 'Bruker oppdatert.',
userMgmtDeleted: 'Bruker slettet.',
userMgmtValidationMissing: 'Brukernavn, visningsnavn og passord er påkrevd.',
userMgmtValidationMissingIdentity: 'Brukernavn og visningsnavn er påkrevd.',
userMgmtMagicStringGenerate: 'Generer magic string',
userMgmtSaveAndMagicString: 'Lagre og hent magic string',
userMgmtMagicStringPasswordNavHint:
'Navidrome lagrer dette passordet for brukeren. Hvis det avviker fra det nåværende, oppdateres innloggingspassordet på serveren.',
userMgmtMagicStringPlaintextWarning:
'Del magic string med varsomhet: den inneholder passordet i klartekst (koding er ikke kryptering). Den som har hele strengen, kan logge inn som denne brukeren.',
userMgmtMagicStringCopied: 'Magic string er kopiert til utklippstavlen.',
userMgmtMagicStringCopyFailed: 'Klarte ikke å kopiere til utklippstavlen.',
userMgmtMagicStringLoginFailed: 'Passordsjekk mislyktes — påloggingsdetaljene kunne ikke verifiseres.',
userMgmtMagicStringModalTitle: 'Generer magic string',
userMgmtMagicStringModalDesc: 'Skriv inn Subsonic-passordet for «{{username}}». Det tas med i den kopierte magic string-en.',
userMgmtMagicStringModalConfirm: 'Kopier streng',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Slå på hvis denne serveren bruker <pluginLink>AudioMuse-AI Navidrome-plugin</pluginLink>. Aktiverer Instant Mix fra spor og henter lignende artister fra serveren i stedet for Last.fm på artistsider.',
+16
View File
@@ -350,6 +350,9 @@ export const nlTranslation = {
urlRequired: 'Voer een server-URL in.',
savedServers: 'Opgeslagen servers',
addNew: 'Of een nieuwe server toevoegen',
orMagicString: 'Of magic string',
magicStringPlaceholder: 'Plak een deelstring (psysonic1-…)',
magicStringInvalid: 'Ongeldige of onleesbare magic string.',
},
connection: {
connected: 'Verbonden',
@@ -498,6 +501,19 @@ export const nlTranslation = {
userMgmtUpdated: 'Gebruiker bijgewerkt.',
userMgmtDeleted: 'Gebruiker verwijderd.',
userMgmtValidationMissing: 'Gebruikersnaam, weergavenaam en wachtwoord zijn vereist.',
userMgmtValidationMissingIdentity: 'Gebruikersnaam en weergavenaam zijn vereist.',
userMgmtMagicStringGenerate: 'Magic string genereren',
userMgmtSaveAndMagicString: 'Opslaan en magic string kopiëren',
userMgmtMagicStringPasswordNavHint:
'Navidrome slaat dit wachtwoord voor de gebruiker op. Als het afwijkt van het huidige wachtwoord, wordt het inlogwachtwoord op de server bijgewerkt.',
userMgmtMagicStringPlaintextWarning:
'Wees voorzichtig met delen: de magic string bevat een onversleuteld wachtwoord (codering is geen encryptie). Wie de volledige string heeft, kan als deze gebruiker inloggen.',
userMgmtMagicStringCopied: 'Magic string naar het klembord gekopieerd.',
userMgmtMagicStringCopyFailed: 'Kopiëren naar het klembord is mislukt.',
userMgmtMagicStringLoginFailed: 'Wachtwoordcontrole mislukt — aanmeldgegevens konden niet worden geverifieerd.',
userMgmtMagicStringModalTitle: 'Magic string genereren',
userMgmtMagicStringModalDesc: 'Voer het Subsonic-wachtwoord voor „{{username}}" in. Het wordt opgenomen in de gekopieerde magic string.',
userMgmtMagicStringModalConfirm: 'String kopiëren',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Zet aan als deze server de <pluginLink>AudioMuse-AI Navidrome-plugin</pluginLink> gebruikt. Schakelt Instant Mix per nummer in en toont vergelijkbare artiesten van de server i.p.v. Last.fm op artiestpaginas.',
+16
View File
@@ -370,6 +370,9 @@ export const ruTranslation = {
urlRequired: 'Укажите адрес сервера.',
savedServers: 'Сохранённые серверы',
addNew: 'Или добавить новый сервер',
orMagicString: 'Или magic string',
magicStringPlaceholder: 'Вставьте строку приглашения (psysonic1-…)',
magicStringInvalid: 'Неверная или нечитаемая magic string.',
},
connection: {
connected: 'Подключено',
@@ -522,6 +525,19 @@ export const ruTranslation = {
userMgmtUpdated: 'Пользователь обновлён.',
userMgmtDeleted: 'Пользователь удалён.',
userMgmtValidationMissing: 'Требуются имя пользователя, отображаемое имя и пароль.',
userMgmtValidationMissingIdentity: 'Укажите имя пользователя и отображаемое имя.',
userMgmtMagicStringGenerate: 'Сгенерировать magic string',
userMgmtSaveAndMagicString: 'Сохранить и получить magic string',
userMgmtMagicStringPasswordNavHint:
'Navidrome сохранит этот пароль для пользователя. Если он не совпадает с текущим, на сервере будет установлен новый пароль входа.',
userMgmtMagicStringPlaintextWarning:
'Передавайте magic string осторожно: в ней пароль в открытом виде (кодирование — не шифрование). У кого есть полная строка, тот может войти под этим пользователем.',
userMgmtMagicStringCopied: 'Magic string скопирован в буфер обмена.',
userMgmtMagicStringCopyFailed: 'Не удалось скопировать в буфер обмена.',
userMgmtMagicStringLoginFailed: 'Пароль не подошёл — не удалось проверить учётные данные.',
userMgmtMagicStringModalTitle: 'Сгенерировать magic string',
userMgmtMagicStringModalDesc: 'Введите пароль Subsonic для «{{username}}» — он попадёт в копируемую magic string.',
userMgmtMagicStringModalConfirm: 'Скопировать строку',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Включите, если на этом сервере настроен <pluginLink>плагин AudioMuse-AI для Navidrome</pluginLink>. Появится Instant Mix для треков, а на странице исполнителя похожие будут браться с сервера вместо Last.fm.',
+16
View File
@@ -350,6 +350,9 @@ export const zhTranslation = {
urlRequired: '请输入服务器地址。',
savedServers: '已保存的服务器',
addNew: '或添加新服务器',
orMagicString: '或使用魔法字符串',
magicStringPlaceholder: '粘贴分享字符串(psysonic1-…)',
magicStringInvalid: '魔法字符串无效或无法解析。',
},
connection: {
connected: '已连接',
@@ -494,6 +497,19 @@ export const zhTranslation = {
userMgmtUpdated: '用户已更新。',
userMgmtDeleted: '用户已删除。',
userMgmtValidationMissing: '用户名、显示名称和密码均为必填项。',
userMgmtValidationMissingIdentity: '用户名和显示名称为必填项。',
userMgmtMagicStringGenerate: '生成魔法字符串',
userMgmtSaveAndMagicString: '保存并获取魔法字符串',
userMgmtMagicStringPasswordNavHint:
'Navidrome 会保存此密码。若与当前密码不同,服务器上的登录密码将被更新。',
userMgmtMagicStringPlaintextWarning:
'请谨慎分享魔法字符串:其中包含未加密的密码(编码不等于加密)。掌握完整字符串的人可以以该用户身份登录。',
userMgmtMagicStringCopied: '魔法字符串已复制到剪贴板。',
userMgmtMagicStringCopyFailed: '无法复制到剪贴板。',
userMgmtMagicStringLoginFailed: '密码验证失败,无法确认凭据。',
userMgmtMagicStringModalTitle: '生成魔法字符串',
userMgmtMagicStringModalDesc: '请输入用户「{{username}}」的 Subsonic 密码,它会包含在复制的魔法字符串中。',
userMgmtMagicStringModalConfirm: '复制字符串',
audiomuseTitle: 'AudioMuse-AINavidrome',
audiomuseDesc:
'若此服务器已配置 <pluginLink>AudioMuse-AI Navidrome 插件</pluginLink>请开启。可从曲目启动即时混音,并在艺人页使用服务器返回的相似艺人,而非 Last.fm。',