Auth: Accept access token as passwd with fail rate limit #782 #808 #3943

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2024-01-14 18:28:17 +01:00
parent 9586a9ec69
commit fed1d8ad95
71 changed files with 930 additions and 507 deletions

View file

@ -403,13 +403,13 @@ export default class Session {
// Use a static auth token in public mode, as no additional authentication is required.
this.setAuthToken(PublicAuthToken);
this.setId(PublicSessionID);
return Api.get("session/" + this.getId()).then((resp) => {
return Api.get("session").then((resp) => {
this.setResp(resp);
return Promise.resolve();
});
} else if (this.isAuthenticated()) {
// Check the auth token by fetching the client session data from the API.
return Api.get("session/" + this.getId())
return Api.get("session")
.then((resp) => {
this.setResp(resp);
return Promise.resolve();
@ -452,7 +452,7 @@ export default class Session {
logout(noRedirect) {
if (this.isAuthenticated()) {
return Api.delete("session/" + this.getId())
return Api.delete("session")
.then(() => {
return this.onLogout(noRedirect);
})

View file

@ -658,8 +658,8 @@ msgstr "Ontfout logs"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Verstek"
@ -1343,7 +1343,7 @@ msgstr "Laaste sinkronisering"
msgid "Latitude"
msgstr "Breedtegraad"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1408,8 +1408,8 @@ msgstr "Leef"
msgid "Live Photos"
msgstr "Regstreekse Foto's"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Plaaslik"
@ -1655,7 +1655,7 @@ msgstr "Geen waarskuwings of foute wat hierdie sleutelwoord bevat nie. Let daaro
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Nie-fotografiese en lae kwaliteit prente vereis 'n hersiening voordat dit in soekresultate verskyn."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Geen"
@ -2157,7 +2157,7 @@ msgstr "Diens-URL"
msgid "Services"
msgstr "Dienste"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sessie"

View file

@ -661,8 +661,8 @@ msgstr "سجلات التصحيح"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "تقصير"
@ -1346,7 +1346,7 @@ msgstr "آخر مزامنة"
msgid "Latitude"
msgstr "خط العرض"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP / AD"
@ -1411,8 +1411,8 @@ msgstr "يعيش"
msgid "Live Photos"
msgstr "Live Photos"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "محلي"
@ -1658,7 +1658,7 @@ msgstr "لا تحذيرات أو خطأ يحتوي على هذه الكلمة ا
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "تتطلب الصور غير الفوتوغرافية وذات الجودة المنخفضة المراجعة قبل ظهورها في نتائج البحث."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "لا أحد"
@ -2160,7 +2160,7 @@ msgstr "URL الخدمة"
msgid "Services"
msgstr "خدمات"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "حصة"

View file

@ -658,8 +658,8 @@ msgstr "Журналы адладкі"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Па змаўчанні"
@ -1343,7 +1343,7 @@ msgstr "Апошняя сінхранізацыя"
msgid "Latitude"
msgstr "Шырата"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1408,8 +1408,8 @@ msgstr "жыць"
msgid "Live Photos"
msgstr "Жывыя фатаграфіі"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Мясцовы"
@ -1655,7 +1655,7 @@ msgstr "Няма папярэджанняў або памылак з гэтым
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Нефатаграфічныя і нізкаякасныя выявы патрабуюць праверкі, перш чым яны з'явяцца ў выніках пошуку."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Няма"
@ -2157,7 +2157,7 @@ msgstr "URL службы"
msgid "Services"
msgstr "Паслугі"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "сесія"

View file

@ -661,8 +661,8 @@ msgstr "Протоколи за отработване"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "По подразбиране"
@ -1346,7 +1346,7 @@ msgstr "Синхронизиране"
msgid "Latitude"
msgstr "Географска ширина"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "На живо"
msgid "Live Photos"
msgstr "Снимки"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Местни"
@ -1658,7 +1658,7 @@ msgstr "Няма предупреждения или грешки, съдърж
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Нефотографските изображения и изображенията с ниско качество изискват преглед, преди да се появят в резултатите от търсенето."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Няма"
@ -2160,7 +2160,7 @@ msgstr "URL адрес на услугата"
msgid "Services"
msgstr "URL адрес на услугата"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Сесия"

View file

@ -661,8 +661,8 @@ msgstr "Registres de depuració"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Per defecte"
@ -1346,7 +1346,7 @@ msgstr "Última sincronització"
msgid "Latitude"
msgstr "Latitud"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "En viu"
msgid "Live Photos"
msgstr "Fotos en directe"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Local"
@ -1658,7 +1658,7 @@ msgstr "No hi ha cap advertiment ni error que contingui aquesta paraula clau. Ti
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Les imatges no fotogràfiques i de baixa qualitat requereixen una revisió abans que apareguin als resultats de la cerca."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Cap"
@ -2160,7 +2160,7 @@ msgstr "URL del servei"
msgid "Services"
msgstr "Serveis"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sessió"

View file

@ -661,8 +661,8 @@ msgstr "Protokoly ladění"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Výchozí"
@ -1346,7 +1346,7 @@ msgstr "Poslední synchronizace"
msgid "Latitude"
msgstr "Zeměpisná šířka"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Živé"
msgid "Live Photos"
msgstr "Živé fotografie"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Místní"
@ -1658,7 +1658,7 @@ msgstr "Žádná varování nebo chyba obsahující toto klíčové slovo. Mějt
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Nefotografické obrázky a snímky nízké kvality vyžadují kontrolu, než se objeví ve výsledcích vyhledávání."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Žádné"
@ -2160,7 +2160,7 @@ msgstr "URL služby"
msgid "Services"
msgstr "Služby"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Relace"

View file

@ -661,8 +661,8 @@ msgstr "Fejlfindingslog"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Standard"
@ -1346,7 +1346,7 @@ msgstr "Seneste synkronisering"
msgid "Latitude"
msgstr "Breddegrad"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Direkte"
msgid "Live Photos"
msgstr "Live-fotos"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokal"
@ -1658,7 +1658,7 @@ msgstr "Ingen advarsler eller fejl, der indeholder dette nøgleord. Bemærk, at
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Ikke-fotografiske billeder af lav kvalitet kræver en gennemgang, før de vises i søgeresultaterne."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Ingen"
@ -2160,7 +2160,7 @@ msgstr "Service-URL"
msgid "Services"
msgstr "Tjenester"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Session"

View file

@ -661,8 +661,8 @@ msgstr "Debug Logs"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Standard"
@ -1346,7 +1346,7 @@ msgstr "Letzte Synchronisation"
msgid "Latitude"
msgstr "Breitengrad"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Live Photos"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokal"
@ -1658,7 +1658,7 @@ msgstr "Keine Warnungen oder Fehler mit diesem Suchbegriff. Bei der Suche wird z
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Nicht-fotografische Inhalte oder Bilder mit geringer Qualität werden erst nach einer Bestätigung in der Suche angezeigt."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Keine"
@ -2160,7 +2160,7 @@ msgstr "Dienst-URL"
msgid "Services"
msgstr "Dienste"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Session"

View file

@ -658,8 +658,8 @@ msgstr "Αρχεία καταγραφής σφαλμάτων"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Προεπιλογή"
@ -1343,7 +1343,7 @@ msgstr "Τελευταίος συγχρονισμός"
msgid "Latitude"
msgstr "Γεωγραφικό πλάτος"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1408,8 +1408,8 @@ msgstr "Ζωντανό"
msgid "Live Photos"
msgstr "Φωτογραφίες"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Τοπικό"
@ -1655,7 +1655,7 @@ msgstr "Δεν υπάρχουν προειδοποιήσεις ή σφάλματ
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Οι μη φωτογραφικές εικόνες και οι εικόνες χαμηλής ποιότητας απαιτούν επανεξέταση προτού εμφανιστούν στα αποτελέσματα αναζήτησης."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Κανένα"
@ -2157,7 +2157,7 @@ msgstr "URL υπηρεσίας"
msgid "Services"
msgstr "URL υπηρεσίας"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Σύνοδος"

View file

@ -660,8 +660,8 @@ msgstr ""
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr ""
@ -1345,7 +1345,7 @@ msgstr ""
msgid "Latitude"
msgstr ""
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr ""
@ -1410,8 +1410,8 @@ msgstr ""
msgid "Live Photos"
msgstr ""
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr ""
@ -1657,7 +1657,7 @@ msgstr ""
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr ""
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr ""
@ -2159,7 +2159,7 @@ msgstr ""
msgid "Services"
msgstr ""
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr ""

View file

@ -661,8 +661,8 @@ msgstr "Registros de depuración"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Por defecto"
@ -1346,7 +1346,7 @@ msgstr "Última sincronización"
msgid "Latitude"
msgstr "Latitud"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1412,8 +1412,8 @@ msgstr "En vivo"
msgid "Live Photos"
msgstr "Fotos en vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Local"
@ -1659,7 +1659,7 @@ msgstr "No hay advertencias ni errores que contengan esta palabra clave. Tenga e
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Las imágenes no fotográficas y de baja calidad requieren una revisión antes que aparezcan en los resultados de la búsqueda."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Ninguno"
@ -2161,7 +2161,7 @@ msgstr "URL del servicio"
msgid "Services"
msgstr "Servicios"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sesión"

View file

@ -658,8 +658,8 @@ msgstr "Tõrkeotsingu logid"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Vaikimisi"
@ -1343,7 +1343,7 @@ msgstr "Viimane sünkroonimine"
msgid "Latitude"
msgstr "Laiuskraad"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1408,8 +1408,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Liikuvad fotod"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Kohalik"
@ -1655,7 +1655,7 @@ msgstr "Seda märksõna sisaldavaid hoiatusi või vigu ei ole. Pane tähele, et
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Mittefotograafilised ja madala kvaliteediga pildid tuleb üle vaadata, enne kui nad otsingutulemustes ilmuvad."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Puudub"
@ -2157,7 +2157,7 @@ msgstr "Teenuse URL"
msgid "Services"
msgstr "Teenused"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sessioon"

View file

@ -658,8 +658,8 @@ msgstr "Arazte-erregistroak"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Lehenetsia"
@ -1343,7 +1343,7 @@ msgstr "Azken sinkronizazioa"
msgid "Latitude"
msgstr "Latitudea"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1408,8 +1408,8 @@ msgstr "Zuzenean"
msgid "Live Photos"
msgstr "Zuzeneko Argazkiak"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Tokikoa"
@ -1655,7 +1655,7 @@ msgstr "Ez dago gako-hitz hau duen abisurik edo errorerik. Kontuan izan bilaketa
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Argazkiak ez diren eta kalitate baxuko irudiak berrikusi behar dira bilaketa-emaitzetan agertu aurretik."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Bat ere ez"
@ -2157,7 +2157,7 @@ msgstr "Zerbitzuaren URLa"
msgid "Services"
msgstr "Zerbitzuak"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Saioa"

View file

@ -661,8 +661,8 @@ msgstr "گزارش‌های اشکال زدایی"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "پیشفرض"
@ -1346,7 +1346,7 @@ msgstr "آخرین همگام سازی"
msgid "Latitude"
msgstr "عرض جغرافیایی"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "زنده"
msgid "Live Photos"
msgstr "تصاویر"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "محلی"
@ -1658,7 +1658,7 @@ msgstr "هیچ هشدار یا خطایی حاوی این کلمه کلیدی ن
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "تصاویر غیرعکاسی و با کیفیت پایین قبل از اینکه در نتایج جستجو ظاهر شوند نیاز به بررسی دارند."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "هیچ یک"
@ -2160,7 +2160,7 @@ msgstr "URL سرویس"
msgid "Services"
msgstr "URL سرویس"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "جلسه"

View file

@ -658,8 +658,8 @@ msgstr "Vianmäärityslokit"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Oletus"
@ -1343,7 +1343,7 @@ msgstr "Viimeisin synkronointi"
msgid "Latitude"
msgstr "Leveysaste"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1408,8 +1408,8 @@ msgstr "Live Photo -kuva"
msgid "Live Photos"
msgstr "Kuvat"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Paikallinen"
@ -1655,7 +1655,7 @@ msgstr "Ei varoituksia tai virheitä, jotka sisältävät tämän avainsanan. Hu
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Muut kuin valokuvat ja heikkolaatuiset kuvat edellyttävät tarkistusta, ennen kuin ne näkyvät hakutuloksissa."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Ei mitään"
@ -2157,7 +2157,7 @@ msgstr "Palvelun URL-osoite"
msgid "Services"
msgstr "Palvelun URL-osoite"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Istunto"

View file

@ -661,8 +661,8 @@ msgstr "Journaux de débogage"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Valeur par défaut"
@ -1346,7 +1346,7 @@ msgstr "Dernière synchro"
msgid "Latitude"
msgstr "Latitude"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Photos en direct"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Locale"
@ -1658,7 +1658,7 @@ msgstr "Aucun avertissement ou erreur contenant ce mot-clé. Notez que la recher
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Les images non photographiques ou de mauvaise qualité doivent faire l'objet d'un examen avant d'apparaître dans les résultats de recherche."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Aucun"
@ -2160,7 +2160,7 @@ msgstr "URL du service"
msgid "Services"
msgstr "Services"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Session"

View file

@ -661,8 +661,8 @@ msgstr "Debug Logs"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "ברירת מחדל"
@ -1346,7 +1346,7 @@ msgstr "סנכרון אחרון"
msgid "Latitude"
msgstr "קו רוחב"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "חי"
msgid "Live Photos"
msgstr "תמונות חיות"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "מְקוֹמִי"
@ -1658,7 +1658,7 @@ msgstr "אין אזהרות או שגיאות המכילות מילת מפתח
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "תמונות שאינן נראות צילום או באיכות נמוכה דורשות בדיקה לפני שהן מופיעות בתוצאות החיפוש."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "ללא"
@ -2160,7 +2160,7 @@ msgstr "נתיב השרות"
msgid "Services"
msgstr "שירותים"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "מוֹשָׁב"

View file

@ -661,8 +661,8 @@ msgstr "दोषमार्जन लॉग"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "चूक"
@ -1346,7 +1346,7 @@ msgstr "अंतिम सिंक"
msgid "Latitude"
msgstr "अक्षांश"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "एलडीएपी/एडी"
@ -1411,8 +1411,8 @@ msgstr "लाइव"
msgid "Live Photos"
msgstr "लाइव तस्वीरें"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "स्थानीय"
@ -1658,7 +1658,7 @@ msgstr "इस कीवर्ड से कोई चेतावनी या
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "गैर-फोटोग्राफिक और निम्न-गुणवत्ता वाली छवियों को खोज परिणामों में प्रदर्शित होने से पहले समीक्षा की आवश्यकता होती है।"
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "कोई नहीं"
@ -2160,7 +2160,7 @@ msgstr "सेवा URL"
msgid "Services"
msgstr "सेवाएं"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "सत्र"

View file

@ -661,8 +661,8 @@ msgstr "Zapisnici otklanjanja pogrešaka"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Zadano"
@ -1346,7 +1346,7 @@ msgstr "Zadnja sinkronizacija"
msgid "Latitude"
msgstr "Zemljopisna širina"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Uživo"
msgid "Live Photos"
msgstr "Slike"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokalni"
@ -1658,7 +1658,7 @@ msgstr "Nema upozorenja ili pogreške koje sadrže ovu ključnu riječ. Imajte n
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Nefotografske slike i slike niske kvalitete zahtijevaju pregled prije nego što se pojave u rezultatima pretraživanja."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Nijedan"
@ -2160,7 +2160,7 @@ msgstr "URL usluge"
msgid "Services"
msgstr "URL usluge"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sjednica"

View file

@ -660,8 +660,8 @@ msgstr "Hibakeresési naplók"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Alapértelmezett"
@ -1345,7 +1345,7 @@ msgstr "Utolsó szinkronizálás"
msgid "Latitude"
msgstr "Szélességi kör"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1410,8 +1410,8 @@ msgstr "Élő"
msgid "Live Photos"
msgstr "Fényképek"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Helyi"
@ -1657,7 +1657,7 @@ msgstr "Nincsenek figyelmeztetések vagy hibák, amelyek ezt a kulcsszót tartal
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "A rossz minőségű képek ellenörzésre kerülnek, mielőtt megjelennének a keresési eredmények között."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Egyik sem"
@ -2159,7 +2159,7 @@ msgstr "Szolgáltatás URL-je"
msgid "Services"
msgstr "Szolgáltatások"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Ülés"

View file

@ -661,8 +661,8 @@ msgstr "Log Debug"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Bawaan"
@ -1346,7 +1346,7 @@ msgstr "Sinkronisasi Terakhir"
msgid "Latitude"
msgstr "Lintang"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Langsung"
msgid "Live Photos"
msgstr "Foto"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokal"
@ -1658,7 +1658,7 @@ msgstr "Tidak ada peringatan atau kesalahan yang mengandung kata kunci ini. Perh
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Gambar non-fotografis dan berkualitas rendah memerlukan peninjauan sebelum muncul di hasil pencarian."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Tidak ada"
@ -2160,7 +2160,7 @@ msgstr "URL Layanan"
msgid "Services"
msgstr "URL Layanan"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sesi"

View file

@ -661,8 +661,8 @@ msgstr "Registri di debug"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Predefinito"
@ -1346,7 +1346,7 @@ msgstr "Ultima sincronizzazione"
msgid "Latitude"
msgstr "Latitudine"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Foto dal vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Locale"
@ -1658,7 +1658,7 @@ msgstr "Nessun warning o errore contiene questa parola chiave. Tieni presente ch
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Le immagini non fotografiche e di bassa qualità richiedono una revisione prima di essere visualizzate nei risultati di ricerca."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Nessuno"
@ -2160,7 +2160,7 @@ msgstr "URL Servizio"
msgid "Services"
msgstr "Servizi"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sessione"

View file

@ -661,8 +661,8 @@ msgstr "デバッグログ"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "既定"
@ -1346,7 +1346,7 @@ msgstr "最終同期"
msgid "Latitude"
msgstr "緯度"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "ライブ"
msgid "Live Photos"
msgstr "ライブ写真"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "ローカル"
@ -1658,7 +1658,7 @@ msgstr "このキーワードを含む警告やエラーは1つも見つかり
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "写真ではないものや、低品質な画像は検索結果に現れる前にレビューが必要です。"
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "なし"
@ -2160,7 +2160,7 @@ msgstr "サービス URL"
msgid "Services"
msgstr "サービス"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "セッション"

View file

@ -661,8 +661,8 @@ msgstr "디버그 로그"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "기본값"
@ -1346,7 +1346,7 @@ msgstr "마지막 동기화"
msgid "Latitude"
msgstr "위도"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "라이브"
msgid "Live Photos"
msgstr "라이브 포토"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "로컬"
@ -1658,7 +1658,7 @@ msgstr "이 키워드를 포함하는 경고 또는 오류가 없습니다. 검
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "사진이 아닌 저품질 이미지는 검색 결과에 표시되기 전에 검토가 필요합니다."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "없음"
@ -2160,7 +2160,7 @@ msgstr "서비스 URL"
msgid "Services"
msgstr "서비스"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "세션"

View file

@ -661,8 +661,8 @@ msgstr "تۆماری هەڵەکان"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "بنه‌ڕه‌ت"
@ -1346,7 +1346,7 @@ msgstr "هاوکاتگەری"
msgid "Latitude"
msgstr "هێڵی پانیی"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "زیندوو"
msgid "Live Photos"
msgstr "وێنەکان"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Herêmî"
@ -1658,7 +1658,7 @@ msgstr "هیچ ئاگادارییەک یان هەڵەیەک نیە کە ئەم
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "وێنە نافۆتۆگرافی و کوالێتی نزمەکان پێویستی بە پێداچونەوە هەیە پێش ئەوەی لە ئەنجامی گەڕاندا دەرکەون."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "هیچ"
@ -2160,7 +2160,7 @@ msgstr "بەستەری خزمەتگوزاری"
msgid "Services"
msgstr "بەستەری خزمەتگوزاری"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Rûniştinî"

View file

@ -661,8 +661,8 @@ msgstr "Derinimo žurnalai"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Numatytoji"
@ -1346,7 +1346,7 @@ msgstr "Sinchronizavimas"
msgid "Latitude"
msgstr "Platuma"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Gyvai"
msgid "Live Photos"
msgstr "Nuotraukos"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Vietinis"
@ -1658,7 +1658,7 @@ msgstr "Jokių įspėjimų ar klaidų su šiuo raktažodžiu nėra. Atkreipkite
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Prieš rodant nefotografuotus ir prastos kokybės vaizdus paieškos rezultatuose, juos reikia peržiūrėti."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Nėra"
@ -2160,7 +2160,7 @@ msgstr "Paslaugos URL"
msgid "Services"
msgstr "Paslaugos URL"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sesija"

View file

@ -661,8 +661,8 @@ msgstr "Log Nyahpepijat"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Lalai"
@ -1346,7 +1346,7 @@ msgstr "Penyegerakan Terakhir"
msgid "Latitude"
msgstr "Latitud"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Langsung"
msgid "Live Photos"
msgstr "Foto"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Tempatan"
@ -1658,7 +1658,7 @@ msgstr "Tiada amaran atau ralat yang mengandungi kata kunci ini. Ambil perhatian
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Imej bukan fotografi dan berkualiti rendah memerlukan semakan sebelum ia muncul dalam hasil carian."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Tiada"
@ -2160,7 +2160,7 @@ msgstr "URL perkhidmatan"
msgid "Services"
msgstr "URL perkhidmatan"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sesi"

View file

@ -661,8 +661,8 @@ msgstr "Feilsøkingslogger"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Standard"
@ -1346,7 +1346,7 @@ msgstr "Siste synkronisering"
msgid "Latitude"
msgstr "Breddegrad"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Direkte"
msgid "Live Photos"
msgstr "Fotoer"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokalt"
@ -1658,7 +1658,7 @@ msgstr "Ingen advarsler eller feilmeldinger inneholder dette nøkkelordet. Merk
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Bilder som ikke er fotografiske eller har lav kvalitet må gjennomgås før de kommer i søkeresultater."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Ingen"
@ -2160,7 +2160,7 @@ msgstr "Tjeneste-URL"
msgid "Services"
msgstr "Tjenester"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sesjon"

View file

@ -661,8 +661,8 @@ msgstr "Debug-logs"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Standaard"
@ -1346,7 +1346,7 @@ msgstr "Laatste synchronisatie"
msgid "Latitude"
msgstr "Breedtegraad"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Live foto's"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokaal"
@ -1658,7 +1658,7 @@ msgstr "Geen waarschuwingen of fouten met dit trefwoord. Let op: zoeken is hoofd
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Niet-fotografische beelden en beelden van lage kwaliteit moeten worden beoordeeld voordat ze in de zoekresultaten verschijnen."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Geen"
@ -2160,7 +2160,7 @@ msgstr "Service URL"
msgid "Services"
msgstr "Diensten"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sessie"

View file

@ -661,8 +661,8 @@ msgstr "Logi debugowania"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Domyślny"
@ -1346,7 +1346,7 @@ msgstr "Ostatnia synchronizacja"
msgid "Latitude"
msgstr "Szerokość geograficzna"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Zdjęcia na żywo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokalnie"
@ -1658,7 +1658,7 @@ msgstr "Brak ostrzeżeń lub błędów zawierających to słowo kluczowe. Zwró
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Obrazy niebędące fotografiami lub posiadające niską jakość wymagają zatwierdzenia, zanim pojawią się w wynikach wyszukiwania."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Brak"
@ -2160,7 +2160,7 @@ msgstr "Adres URL do usługi"
msgid "Services"
msgstr "Usługi"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sesja"

View file

@ -661,8 +661,8 @@ msgstr "Registros de depuração"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Padrão"
@ -1346,7 +1346,7 @@ msgstr "Última Sincronia"
msgid "Latitude"
msgstr "Latitude"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Ao vivo"
msgid "Live Photos"
msgstr "Fotos ao vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Local"
@ -1658,7 +1658,7 @@ msgstr "Nenhum alerta ou erro contendo esta palavra-chave. Note que a pesquisa d
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Imagens de baixa qualidade ou não-fotográficas necessitam de revisão antes de aparecerem nos resultados da pesquisa."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Nenhum"
@ -2160,7 +2160,7 @@ msgstr "URL do serviço"
msgid "Services"
msgstr "Serviços"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sessão"

View file

@ -661,8 +661,8 @@ msgstr "Registros de depuração"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Padrão"
@ -1346,7 +1346,7 @@ msgstr "Última Sincronia"
msgid "Latitude"
msgstr "Latitude"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Ao vivo"
msgid "Live Photos"
msgstr "Fotos ao vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Local"
@ -1658,7 +1658,7 @@ msgstr "Nenhum alerta ou erro contento esta palavra-chave. Note que a busca dife
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Imagens de baixa qualidade ou não-fotográficas necessitam de revisão antes de aparecerem nos resultados de busca."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Nenhum"
@ -2160,7 +2160,7 @@ msgstr "URL do serviço"
msgid "Services"
msgstr "Serviços"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sessão"

View file

@ -661,8 +661,8 @@ msgstr "Jurnalele de depanare"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Implicit"
@ -1346,7 +1346,7 @@ msgstr "Ultima sincronizare"
msgid "Latitude"
msgstr "Latitudine"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Fotografii în direct"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Local"
@ -1658,7 +1658,7 @@ msgstr "Nu există avertismente sau erori care să conțină acest cuvânt cheie
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Imaginile nefotografice și de slabă calitate necesită o revizuire înainte de a apărea în rezultatele căutării."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Nici unul"
@ -2160,7 +2160,7 @@ msgstr "URL de serviciu"
msgid "Services"
msgstr "Servicii"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sesiunea"

View file

@ -661,8 +661,8 @@ msgstr "Отладочные Логи"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "По умолчанию"
@ -1346,7 +1346,7 @@ msgstr "Последняя синхронизация"
msgid "Latitude"
msgstr "Широта"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Прямой эфир"
msgid "Live Photos"
msgstr "Живые фотографии"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Местный"
@ -1658,7 +1658,7 @@ msgstr "Нет предупреждений или ошибок содержащ
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Файлы, не являющиеся фотографиями, или изображения низкого качества нужно одобрить, чтобы они появились в результатах поиска."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Ничего"
@ -2160,7 +2160,7 @@ msgstr "URL сервиса"
msgid "Services"
msgstr "Сервисы"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Сессия"

View file

@ -661,8 +661,8 @@ msgstr "Denníky ladenia"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Predvolená"
@ -1346,7 +1346,7 @@ msgstr "Posledná synchronizácia"
msgid "Latitude"
msgstr "Šírka"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Živé"
msgid "Live Photos"
msgstr "Živé fotografie"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Miestne stránky"
@ -1658,7 +1658,7 @@ msgstr "Nenašli sa žiadne upozornenia ani chyby ktoré by obsahovali toto kľ
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Nefotografické a fotografie nízkej kvality vyžadujú skontrolovanie pred tým než sa zobrazia vo výsledkoch vyhľadávania."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Žiadne"
@ -2160,7 +2160,7 @@ msgstr "URL Služby"
msgid "Services"
msgstr "Služby"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Zasadnutie"

View file

@ -658,8 +658,8 @@ msgstr "Dnevniki za odpravljanje napak"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Privzeto"
@ -1343,7 +1343,7 @@ msgstr "Zadnja sinhronizacija"
msgid "Latitude"
msgstr "Zemljepisna širina"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1408,8 +1408,8 @@ msgstr "V živo"
msgid "Live Photos"
msgstr "Fotografije"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokalni"
@ -1655,7 +1655,7 @@ msgstr "Ni opozoril ali napak, ki bi vsebovale to ključno besedo. Upoštevajte,
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Nefotografske slike in slike nizke kakovosti je treba pred prikazom v rezultatih iskanja pregledati."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Ni"
@ -2157,7 +2157,7 @@ msgstr "URL storitve"
msgid "Services"
msgstr "URL storitve"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Seja"

View file

@ -661,8 +661,8 @@ msgstr "Felsökningsloggar"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Standard"
@ -1346,7 +1346,7 @@ msgstr "Senaste synkronisering"
msgid "Latitude"
msgstr "Latitud"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos"
msgstr "Foton"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Lokal"
@ -1658,7 +1658,7 @@ msgstr "Inga varningar eller fel som innehåller detta nyckelord. Observera att
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Bilder som inte är fotografiska eller av låg kvalitet måste granskas innan de visas i sökresultaten."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Ingen"
@ -2160,7 +2160,7 @@ msgstr "Tjänstens URL"
msgid "Services"
msgstr "Tjänstens URL"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Sammanträde"

View file

@ -661,8 +661,8 @@ msgstr "บันทึกการดีบัก"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "ค่าเริ่มต้น"
@ -1346,7 +1346,7 @@ msgstr "ซิงค์ล่าสุด"
msgid "Latitude"
msgstr "ละติจูด"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "แอลดีเอพี/ค.ศ"
@ -1411,8 +1411,8 @@ msgstr "สด"
msgid "Live Photos"
msgstr "ภาพถ่าย"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "ท้องถิ่น"
@ -1658,7 +1658,7 @@ msgstr "ไม่มีคำเตือนหรือข้อผิดพล
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "รูปภาพที่ไม่ใช่ภาพถ่ายและคุณภาพต่ำต้องได้รับการตรวจสอบก่อนที่จะปรากฏในผลการค้นหา"
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "ไม่มี"
@ -2160,7 +2160,7 @@ msgstr "URL บริการ"
msgid "Services"
msgstr "URL บริการ"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "การประชุม"

View file

@ -661,8 +661,8 @@ msgstr "Hata Kayıtları"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "Varsayılan"
@ -1346,7 +1346,7 @@ msgstr "Son Senkronizasyon"
msgid "Latitude"
msgstr "Enlem"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Canlı"
msgid "Live Photos"
msgstr "Canlı Fotoğraflar"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Yerel"
@ -1658,7 +1658,7 @@ msgstr "Bu anahtar kelimeyi içeren uyarı veya hata yok. Aramanın büyük/kü
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Fotoğrafik olmayan ve düşük kaliteli görseller, arama sonuçlarında görünmeden önce bir inceleme gerektirir."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Hiçbiri"
@ -2160,7 +2160,7 @@ msgstr "Hizmet URL'si"
msgid "Services"
msgstr "Hizmetler"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Oturum"

View file

@ -773,8 +773,8 @@ msgstr ""
#: src/options/admin.js:45
#: src/options/admin.js:59
#: src/options/admin.js:60
#: src/options/admin.js:73
#: src/options/admin.js:92
#: src/options/admin.js:74
#: src/options/admin.js:93
#: src/options/options.js:313
#: src/options/options.js:377
#: src/options/themes.js:492
@ -1579,7 +1579,7 @@ msgid "Latitude"
msgstr ""
#: src/options/admin.js:49
#: src/options/admin.js:81
#: src/options/admin.js:82
msgid "LDAP/AD"
msgstr ""
@ -1664,8 +1664,8 @@ msgstr ""
#: src/options/admin.js:46
#: src/options/admin.js:48
#: src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr ""
@ -1977,8 +1977,8 @@ msgid "Non-photographic and low-quality images require a review before they appe
msgstr ""
#: src/options/admin.js:52
#: src/options/admin.js:85
#: src/options/admin.js:100
#: src/options/admin.js:86
#: src/options/admin.js:101
#: src/options/options.js:293
#: src/options/options.js:389
msgid "None"
@ -2564,6 +2564,7 @@ msgid "Services"
msgstr ""
#: src/model/session.js:98
#: src/options/admin.js:62
msgid "Session"
msgstr ""

View file

@ -661,8 +661,8 @@ msgstr "Журнали налагодження"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "За замовчуванням"
@ -1346,7 +1346,7 @@ msgstr "Остання синхронізація"
msgid "Latitude"
msgstr "Широта"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "Live фото"
msgid "Live Photos"
msgstr "Живі фото"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "Місцевий"
@ -1658,7 +1658,7 @@ msgstr "Немає попереджень або помилок із цим кл
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "Нефотографічні та низькоякісні зображення потребують перевірки, перш ніж з’являться в результатах пошуку."
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "Жодного"
@ -2160,7 +2160,7 @@ msgstr "URL служби"
msgid "Services"
msgstr "Послуги"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "Сесія"

View file

@ -661,8 +661,8 @@ msgstr "调试日志"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "默认"
@ -1346,7 +1346,7 @@ msgstr "上次同步"
msgid "Latitude"
msgstr "纬度"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "实况"
msgid "Live Photos"
msgstr "现场照片"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "当地"
@ -1658,7 +1658,7 @@ msgstr "没有包含此关键字的警告或错误,请注意,搜索区分大
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "非照片和低质量图像出现在搜索结果中前需要进行审查。"
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "无"
@ -2162,7 +2162,7 @@ msgstr "服务 URL"
msgid "Services"
msgstr "服务"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "会议"

View file

@ -661,8 +661,8 @@ msgstr "除錯紀錄"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
#: src/options/admin.js:92 src/options/options.js:313
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:93 src/options/options.js:313
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default"
msgstr "預設"
@ -1346,7 +1346,7 @@ msgstr "上次同步"
msgid "Latitude"
msgstr "緯度"
#: src/options/admin.js:49 src/options/admin.js:81
#: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD"
msgstr "LDAP/AD"
@ -1411,8 +1411,8 @@ msgstr "即時"
msgid "Live Photos"
msgstr "原況照片"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
#: src/options/admin.js:96
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:97
msgid "Local"
msgstr "当地"
@ -1658,7 +1658,7 @@ msgstr "沒有包含此關鍵字的警告或錯誤。請注意,搜尋區分大
msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "非照片和低品質圖像需要進行手動確認,才會出現在搜尋結果中。"
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
#: src/options/options.js:293 src/options/options.js:389
msgid "None"
msgstr "無"
@ -2160,7 +2160,7 @@ msgstr "服務 URL"
msgid "Services"
msgstr "服務"
#: src/model/session.js:98
#: src/model/session.js:98 src/options/admin.js:62
msgid "Session"
msgstr "工作階段"

View file

@ -91,7 +91,7 @@ export class Session extends RestModel {
}
static getCollectionResource() {
return "session";
return "sessions";
}
static getModelName() {

View file

@ -59,6 +59,7 @@ export const AuthMethods = () => {
"": $gettext("Default"),
default: $gettext("Default"),
access_token: $gettext("Access Token"),
session: $gettext("Session"),
"2fa": "2FA",
oauth2: "OAuth2",
oidc: "OIDC",

View file

@ -7,6 +7,14 @@ import (
)
func TestACL_Allow(t *testing.T) {
t.Run("ResourceSessions", func(t *testing.T) {
assert.True(t, Resources.Allow(ResourceSessions, RoleAdmin, AccessAll))
assert.True(t, Resources.Allow(ResourceSessions, RoleAdmin, AccessOwn))
assert.False(t, Resources.Allow(ResourceSessions, RoleVisitor, AccessAll))
assert.True(t, Resources.Allow(ResourceSessions, RoleVisitor, AccessOwn))
assert.False(t, Resources.Allow(ResourceSessions, RoleClient, AccessAll))
assert.True(t, Resources.Allow(ResourceSessions, RoleClient, AccessOwn))
})
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
assert.True(t, Resources.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
})
@ -124,6 +132,6 @@ func TestACL_DenyAll(t *testing.T) {
func TestACL_Resources(t *testing.T) {
t.Run("Resources", func(t *testing.T) {
result := Resources.Resources()
assert.Len(t, result, 21)
assert.Len(t, result, 22)
})
}

View file

@ -21,6 +21,7 @@ const (
ResourcePassword Resource = "password"
ResourceServices Resource = "services"
ResourceUsers Resource = "users"
ResourceSessions Resource = "sessions"
ResourceLogs Resource = "logs"
ResourceWebDAV Resource = "webdav"
ResourceMetrics Resource = "metrics"

View file

@ -61,7 +61,12 @@ var Resources = ACL{
RoleAdmin: GrantFullAccess,
},
ResourceUsers: Roles{
RoleAdmin: Grant{AccessAll: true, AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
RoleAdmin: Grant{AccessAll: true, AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
RoleClient: Grant{AccessOwn: true, ActionView: true},
},
ResourceSessions: Roles{
RoleAdmin: GrantFullAccess,
RoleDefault: Grant{AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
},
ResourceLogs: Roles{
RoleAdmin: GrantFullAccess,

View file

@ -18,16 +18,16 @@ func Auth(c *gin.Context, resource acl.Resource, grant acl.Permission) *entity.S
// AuthAny checks if the user is authorized to access a resource with any of the specified permissions
// and returns the session or nil otherwise.
func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *entity.Session) {
// Get the client IP and session ID from the request headers.
ip := ClientIP(c)
// Get client IP and auth token from the request headers.
clientIp := ClientIP(c)
authToken := AuthToken(c)
// Find active session to perform authorization check or deny if no session was found.
if s = Session(authToken); s == nil {
event.AuditWarn([]string{ip, "unauthenticated", "%s %s", "denied"}, grants.String(), string(resource))
if s = Session(clientIp, authToken); s == nil {
event.AuditWarn([]string{clientIp, "unauthenticated", "%s %s", "denied"}, grants.String(), string(resource))
return entity.SessionStatusUnauthorized()
} else {
s.SetClientIP(ip)
s.SetClientIP(clientIp)
}
// If the request is from a client application, check its authorization based
@ -35,31 +35,31 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
if s.IsClient() {
// Check ACL resource name against the permitted scope.
if !s.HasScope(resource.String()) {
event.AuditErr([]string{ip, "client %s", "session %s", "access %s", "denied"}, s.AuthID, s.RefID, string(resource))
event.AuditErr([]string{clientIp, "client %s", "session %s", "access %s", "denied"}, s.AuthID, s.RefID, string(resource))
return s
}
// Perform an authorization check based on the ACL defaults for client applications.
if acl.Resources.DenyAll(resource, acl.RoleClient, grants) {
event.AuditErr([]string{ip, "client %s", "session %s", "%s %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
return entity.SessionStatusForbidden()
}
// Additionally check the user authorization if the client belongs to a user account.
if s.NoUser() {
// Allow access based on the ACL defaults for client applications.
event.AuditInfo([]string{ip, "client %s", "session %s", "%s %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource))
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource))
} else if u := s.User(); !u.IsDisabled() && !u.IsUnknown() && u.IsRegistered() {
if acl.Resources.DenyAll(resource, u.AclRole(), grants) {
event.AuditErr([]string{ip, "client %s", "session %s", "%s %s as %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
return entity.SessionStatusForbidden()
}
// Allow access based on the user role.
event.AuditInfo([]string{ip, "client %s", "session %s", "%s %s as %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
} else {
// Deny access if it is not a regular user account or the account has been disabled.
event.AuditErr([]string{ip, "client %s", "session %s", "%s %s as unauthorized user", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as unauthorized user", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
return entity.SessionStatusForbidden()
}
@ -68,13 +68,13 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
// Otherwise, perform a regular ACL authorization check based on the user role.
if u := s.User(); u.IsUnknown() || u.IsDisabled() {
event.AuditWarn([]string{ip, "session %s", "%s %s as unauthorized user", "denied"}, s.RefID, grants.String(), string(resource))
event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", "denied"}, s.RefID, grants.String(), string(resource))
return entity.SessionStatusUnauthorized()
} else if acl.Resources.DenyAll(resource, u.AclRole(), grants) {
event.AuditErr([]string{ip, "session %s", "%s %s as %s", "denied"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", "denied"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
return entity.SessionStatusForbidden()
} else {
event.AuditInfo([]string{ip, "session %s", "%s %s as %s", "granted"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", "granted"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
return s
}
}

View file

@ -19,13 +19,13 @@ func UpdateClientConfig() {
// GET /api/v1/config
func GetClientConfig(router *gin.RouterGroup) {
router.GET("/config", func(c *gin.Context) {
s := Session(AuthToken(c))
sess := Session(ClientIP(c), AuthToken(c))
conf := get.Config()
if s == nil {
if sess == nil {
c.JSON(http.StatusOK, conf.ClientPublic())
} else {
c.JSON(http.StatusOK, conf.ClientSession(s))
c.JSON(http.StatusOK, conf.ClientSession(sess))
}
})
}

View file

@ -31,9 +31,9 @@ func AddDownloadHeader(c *gin.Context, fileName string) {
c.Header(header.ContentDisposition, fmt.Sprintf("attachment; filename=%s", fileName))
}
// AddSessionHeader adds a session id header to the response.
func AddSessionHeader(c *gin.Context, id string) {
c.Header(header.XSessionID, id)
// AddAuthTokenHeader adds an X-Auth-Token header to the response.
func AddAuthTokenHeader(c *gin.Context, authToken string) {
c.Header(header.XAuthToken, authToken)
}
// AddContentTypeHeader adds a content type header to the response.

View file

@ -109,7 +109,7 @@ func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, pas
Password: password,
}))
authToken = r.Header().Get(header.XSessionID)
authToken = r.Header().Get(header.XAuthToken)
return
}

View file

@ -3,22 +3,33 @@ package api
import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/rnd"
)
// Session finds the client session for the specified
// auth token, or returns nil if not found.
func Session(authToken string) *entity.Session {
// Session finds the client session for the specified auth token, or returns nil if not found.
func Session(clientIp, authToken string) *entity.Session {
// Skip authentication when running in public mode.
if get.Config().Public() {
return get.Session().Public()
} else if !rnd.IsAuthAny(authToken) {
}
// Fail if the auth token does not have a supported format.
if !rnd.IsAuthAny(authToken) {
return nil
}
// Find the session based on the hashed auth
// token used as id, or return nil otherwise.
if s, err := get.Session().Get(rnd.SessionID(authToken)); err != nil {
// Fail if authentication error rate limit is exceeded.
if clientIp != "" && limiter.Auth.Reject(clientIp) {
return nil
}
// Find the session based on the hashed auth token, or return nil otherwise.
if s, err := entity.FindSession(rnd.SessionID(authToken)); err != nil {
if clientIp != "" {
limiter.Auth.Reserve(clientIp)
}
return nil
} else {
return s

View file

@ -11,17 +11,24 @@ import (
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/header"
)
// CreateSession creates a new client session and returns it as JSON if authentication was successful.
//
// POST /api/v1/session
// POST /api/v1/sessions
func CreateSession(router *gin.RouterGroup) {
router.POST("/session", func(c *gin.Context) {
createSessionHandler := func(c *gin.Context) {
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
var f form.Login
clientIp := ClientIP(c)
if err := c.BindJSON(&f); err != nil {
event.AuditWarn([]string{ClientIP(c), "create session", "invalid request", "%s"}, err)
event.AuditWarn([]string{clientIp, "create session", "invalid request", "%s"}, err)
AbortBadRequest(c)
return
}
@ -40,8 +47,8 @@ func CreateSession(router *gin.RouterGroup) {
return
}
// Check limit for failed auth requests (max. 10 per minute).
if limiter.Login.Reject(ClientIP(c)) {
// Fail if authentication error rate limit is exceeded.
if clientIp != "" && (limiter.Login.Reject(clientIp) || limiter.Auth.Reject(clientIp)) {
limiter.AbortJSON(c)
return
}
@ -50,7 +57,7 @@ func CreateSession(router *gin.RouterGroup) {
var isNew bool
// Find existing session, if any.
if s := Session(AuthToken(c)); s != nil {
if s := Session(clientIp, AuthToken(c)); s != nil {
// Update existing session.
sess = s
} else {
@ -64,25 +71,28 @@ func CreateSession(router *gin.RouterGroup) {
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return
} else if sess, err = get.Session().Save(sess); err != nil {
event.AuditErr([]string{ClientIP(c), "%s"}, err)
event.AuditErr([]string{clientIp, "%s"}, err)
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return
} else if sess == nil {
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
return
} else if isNew {
event.AuditInfo([]string{ClientIP(c), "session %s", "created"}, sess.RefID)
event.AuditInfo([]string{clientIp, "session %s", "created"}, sess.RefID)
} else {
event.AuditInfo([]string{ClientIP(c), "session %s", "updated"}, sess.RefID)
event.AuditInfo([]string{clientIp, "session %s", "updated"}, sess.RefID)
}
// Add session id to response headers.
AddSessionHeader(c, sess.AuthToken())
// Add auth token to response header.
AddAuthTokenHeader(c, sess.AuthToken())
// Response includes user data, session data, and client config values.
response := CreateSessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess))
// Return JSON response.
c.JSON(sess.HttpStatus(), response)
})
}
router.POST("/session", createSessionHandler)
router.POST("/sessions", createSessionHandler)
}

View file

@ -10,8 +10,10 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/internal/session"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -19,8 +21,12 @@ import (
//
// DELETE /api/v1/session
// DELETE /api/v1/session/:id
// DELETE /api/v1/sessions/:id
func DeleteSession(router *gin.RouterGroup) {
deleteSessionHandler := func(c *gin.Context) {
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
// Abort if running in public mode.
if get.Config().Public() {
// Return JSON response for confirmation.
@ -30,13 +36,23 @@ func DeleteSession(router *gin.RouterGroup) {
id := clean.ID(c.Param("id"))
// Get client IP address for logs and rate limiting checks.
clientIP := ClientIP(c)
// Get client IP and auth token from request headers.
clientIp := ClientIP(c)
authToken := AuthToken(c)
// Fail if authentication error rate limit is exceeded.
if clientIp != "" && limiter.Auth.Reject(clientIp) {
limiter.AbortJSON(c)
return
}
// Find session based on auth token.
sess, err := entity.FindSession(rnd.SessionID(AuthToken(c)))
sess, err := entity.FindSession(rnd.SessionID(authToken))
if err != nil || sess == nil {
if clientIp != "" {
limiter.Auth.Reserve(clientIp)
}
Abort(c, http.StatusUnauthorized, i18n.ErrUnauthorized)
return
} else if sess.Abort(c) {
@ -45,29 +61,29 @@ func DeleteSession(router *gin.RouterGroup) {
// Only admins may delete other sessions by ref id.
if rnd.IsRefID(id) {
if !acl.Resources.AllowAll(acl.ResourceUsers, sess.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
event.AuditErr([]string{clientIP, "session %s", "delete session %s as %s", "denied"}, sess.RefID, id, sess.User().AclRole())
if !acl.Resources.AllowAll(acl.ResourceSessions, sess.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", "denied"}, sess.RefID, acl.ResourceSessions.String(), sess.User().AclRole())
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return
}
event.AuditInfo([]string{clientIP, "session %s", "delete session %s as %s", "granted"}, sess.RefID, id, sess.User().AclRole())
event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", "granted"}, sess.RefID, acl.ResourceSessions.String(), sess.User().AclRole())
if sess = entity.FindSessionByRefID(id); sess == nil {
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
return
}
} else if id != "" && sess.ID != id {
event.AuditWarn([]string{clientIP, "session %s", "delete session as %s", "ids do not match"}, sess.RefID, sess.User().AclRole())
event.AuditWarn([]string{clientIp, "session %s", "delete %s as %s", "ids do not match"}, sess.RefID, acl.ResourceSessions.String(), sess.User().AclRole())
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return
}
// Delete session cache and database record.
if err = sess.Delete(); err != nil {
event.AuditErr([]string{clientIP, "session %s", "delete session as %s", "%s"}, sess.RefID, sess.User().AclRole(), err)
event.AuditErr([]string{clientIp, "session %s", "delete session as %s", "%s"}, sess.RefID, sess.User().AclRole(), err)
} else {
event.AuditDebug([]string{clientIP, "session %s", "deleted"}, sess.RefID)
event.AuditDebug([]string{clientIp, "session %s", "deleted"}, sess.RefID)
}
// Return JSON response for confirmation.
@ -76,4 +92,5 @@ func DeleteSession(router *gin.RouterGroup) {
router.DELETE("/session", deleteSessionHandler)
router.DELETE("/session/:id", deleteSessionHandler)
router.DELETE("/sessions/:id", deleteSessionHandler)
}

View file

@ -7,24 +7,34 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/rnd"
)
// GetSession returns the session data as JSON if authentication was successful.
//
// GET /api/v1/session
// GET /api/v1/session/:id
// GET /api/v1/sessions/:id
func GetSession(router *gin.RouterGroup) {
getSessionHandler := func(c *gin.Context) {
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
id := clean.ID(c.Param("id"))
// Check authentication token.
if id == "" {
// Abort if authentication token is missing or empty.
// Abort if session id is provided but invalid.
if id != "" && !rnd.IsSessionID(id) {
AbortBadRequest(c)
return
}
conf := get.Config()
// Get client IP and auth token from request headers.
clientIp := ClientIP(c)
authToken := AuthToken(c)
// Skip authentication if app is running in public mode.
@ -33,18 +43,25 @@ func GetSession(router *gin.RouterGroup) {
sess = get.Session().Public()
id = sess.ID
authToken = sess.AuthToken()
} else if clientIp != "" && limiter.Auth.Reject(clientIp) {
// Fail if authentication error rate limit is exceeded.
limiter.AbortJSON(c)
return
} else {
sess = Session(authToken)
sess = Session(clientIp, authToken)
}
switch {
case sess == nil:
if clientIp != "" {
limiter.Auth.Reserve(clientIp)
}
AbortUnauthorized(c)
return
case sess.Expired(), sess.ID == "":
AbortUnauthorized(c)
return
case sess.Invalid(), sess.ID != id && !conf.Public():
case sess.Invalid(), id != "" && sess.ID != id && !conf.Public():
AbortForbidden(c)
return
}
@ -52,8 +69,8 @@ func GetSession(router *gin.RouterGroup) {
// Update user information.
sess.RefreshUser()
// Add session id to response headers.
AddSessionHeader(c, authToken)
// Add auth token to response header.
AddAuthTokenHeader(c, authToken)
// Response includes user data, session data, and client config values.
response := GetSessionResponse(authToken, sess, get.Config().ClientSession(sess))
@ -62,5 +79,7 @@ func GetSession(router *gin.RouterGroup) {
c.JSON(http.StatusOK, response)
}
router.GET("/session", getSessionHandler)
router.GET("/session/:id", getSessionHandler)
router.GET("/sessions/:id", getSessionHandler)
}

View file

@ -25,12 +25,15 @@ import (
// POST /api/v1/oauth/token
func CreateOAuthToken(router *gin.RouterGroup) {
router.POST("/oauth/token", func(c *gin.Context) {
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
// Get client IP address for logs and rate limiting checks.
clientIP := ClientIP(c)
clientIp := ClientIP(c)
// Abort if running in public mode.
if get.Config().Public() {
event.AuditErr([]string{clientIP, "create client session", "disabled in public mode"})
event.AuditErr([]string{clientIp, "create client session", "disabled in public mode"})
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return
}
@ -45,20 +48,20 @@ func CreateOAuthToken(router *gin.RouterGroup) {
f.ClientID = clientId
f.ClientSecret = clientSecret
} else if err = c.ShouldBind(&f); err != nil {
event.AuditWarn([]string{clientIP, "create client session", "%s"}, err)
event.AuditWarn([]string{clientIp, "create client session", "%s"}, err)
AbortBadRequest(c)
return
}
// Check the credentials for completeness and the correct format.
if err = f.Validate(); err != nil {
event.AuditWarn([]string{clientIP, "create client session", "%s"}, err)
event.AuditWarn([]string{clientIp, "create client session", "%s"}, err)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return
}
// Check limit for failed auth requests (max. 10 per minute).
if limiter.Login.Reject(clientIP) {
// Fail if authentication error rate limit is exceeded.
if clientIp != "" && (limiter.Login.Reject(clientIp) || limiter.Auth.Reject(clientIp)) {
limiter.AbortJSON(c)
return
}
@ -68,22 +71,22 @@ func CreateOAuthToken(router *gin.RouterGroup) {
// Abort if the client ID or secret are invalid.
if client == nil {
event.AuditWarn([]string{clientIP, "client %s", "create session", "invalid client_id"}, f.ClientID)
event.AuditWarn([]string{clientIp, "client %s", "create session", "invalid client id"}, f.ClientID)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
limiter.Login.Reserve(clientIP)
limiter.Login.Reserve(clientIp)
return
} else if !client.AuthEnabled {
event.AuditWarn([]string{clientIP, "client %s", "create session", "authentication disabled"}, f.ClientID)
event.AuditWarn([]string{clientIp, "client %s", "create session", "authentication disabled"}, f.ClientID)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return
} else if method := client.Method(); !method.IsDefault() && method != authn.MethodOAuth2 {
event.AuditWarn([]string{clientIP, "client %s", "create session", "method %s not supported"}, f.ClientID, clean.LogQuote(method.String()))
event.AuditWarn([]string{clientIp, "client %s", "create session", "method %s not supported"}, f.ClientID, clean.LogQuote(method.String()))
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return
} else if client.WrongSecret(f.ClientSecret) {
event.AuditWarn([]string{clientIP, "client %s", "create session", "invalid client_secret"}, f.ClientID)
event.AuditWarn([]string{clientIp, "client %s", "create session", "invalid client secret"}, f.ClientID)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
limiter.Login.Reserve(clientIP)
limiter.Login.Reserve(clientIp)
return
}
@ -92,20 +95,20 @@ func CreateOAuthToken(router *gin.RouterGroup) {
// Try to log in and save session if successful.
if sess, err = get.Session().Save(sess); err != nil {
event.AuditErr([]string{clientIP, "client %s", "create session", "%s"}, f.ClientID, err)
event.AuditErr([]string{clientIp, "client %s", "create session", "%s"}, f.ClientID, err)
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return
} else if sess == nil {
event.AuditErr([]string{clientIP, "client %s", "create session", StatusFailed.String()}, f.ClientID)
event.AuditErr([]string{clientIp, "client %s", "create session", StatusFailed.String()}, f.ClientID)
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
return
} else {
event.AuditInfo([]string{clientIP, "client %s", "session %s", "created"}, f.ClientID, sess.RefID)
event.AuditInfo([]string{clientIp, "client %s", "session %s", "created"}, f.ClientID, sess.RefID)
}
// Deletes old client sessions above the configured limit.
if deleted := client.EnforceAuthTokenLimit(); deleted > 0 {
event.AuditInfo([]string{clientIP, "client %s", "%s deleted"}, f.ClientID, english.Plural(deleted, "old session", "old sessions"))
event.AuditInfo([]string{clientIp, "client %s", "%s deleted"}, f.ClientID, english.Plural(deleted, "old session", "old sessions"))
}
// Response includes access token, token type, and token lifetime.
@ -125,12 +128,15 @@ func CreateOAuthToken(router *gin.RouterGroup) {
// POST /api/v1/oauth/revoke
func RevokeOAuthToken(router *gin.RouterGroup) {
router.POST("/oauth/revoke", func(c *gin.Context) {
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
// Get client IP address for logs and rate limiting checks.
clientIP := ClientIP(c)
clientIp := ClientIP(c)
// Abort if running in public mode.
if get.Config().Public() {
event.AuditErr([]string{clientIP, "delete client session", "disabled in public mode"})
event.AuditErr([]string{clientIp, "delete client session", "disabled in public mode"})
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return
}
@ -144,7 +150,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
// Get the auth token to be revoked from the submitted form values or the request header.
if err = c.ShouldBind(&f); err != nil && authToken == "" {
event.AuditWarn([]string{clientIP, "delete client session", "%s"}, err)
event.AuditWarn([]string{clientIp, "delete client session", "%s"}, err)
AbortBadRequest(c)
return
} else if f.Empty() {
@ -154,7 +160,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
// Check the token form values.
if err = f.Validate(); err != nil {
event.AuditWarn([]string{clientIP, "delete client session", "%s"}, err)
event.AuditWarn([]string{clientIp, "delete client session", "%s"}, err)
AbortBadRequest(c)
return
}
@ -163,28 +169,28 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
sess, err := entity.FindSession(rnd.SessionID(f.AuthToken))
if err != nil {
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err.Error())
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err.Error())
c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized))
return
} else if sess == nil {
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized))
return
} else if sess.Abort(c) {
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
return
} else if !sess.IsClient() {
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
c.AbortWithStatusJSON(http.StatusForbidden, i18n.NewResponse(http.StatusForbidden, i18n.ErrForbidden))
return
} else {
event.AuditInfo([]string{clientIP, "client %s", "session %s", "delete session as %s", "granted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
event.AuditInfo([]string{clientIp, "client %s", "session %s", "delete session as %s", "granted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
}
// Delete session cache and database record.
if err = sess.Delete(); err != nil {
// Log error.
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err)
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err)
// Return JSON error.
c.AbortWithStatusJSON(http.StatusNotFound, i18n.NewResponse(http.StatusNotFound, i18n.ErrNotFound))
@ -192,7 +198,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
}
// Log event.
event.AuditInfo([]string{clientIP, "client %s", "session %s", "deleted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID))
event.AuditInfo([]string{clientIp, "client %s", "session %s", "deleted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID))
// Return JSON response for confirmation.
c.JSON(http.StatusOK, DeleteSessionResponse(sess.ID))

View file

@ -17,8 +17,8 @@ import (
func TestSession(t *testing.T) {
t.Run("Public", func(t *testing.T) {
sess := get.Session().Public()
assert.Equal(t, sess, Session(""))
assert.Equal(t, sess, Session("638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
assert.Equal(t, sess, Session("1.2.3.4", ""))
assert.Equal(t, sess, Session("1.2.3.4", "1234ffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
})
}
@ -213,13 +213,43 @@ func TestGetSession(t *testing.T) {
GetSession(router)
authToken := AuthenticateAdmin(app, router)
t.Logf("Session ID: %s", authToken)
t.Logf("Auth Token: %s", authToken)
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session", authToken)
t.Logf("Response Body: %s", r.Body.String())
id := gjson.Get(r.Body.String(), "session_id").String()
assert.Equal(t, rnd.SessionID(authToken), id)
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("AdminAuthenticatedRequestWithID", func(t *testing.T) {
app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
GetSession(router)
authToken := AuthenticateAdmin(app, router)
t.Logf("Auth Token: %s", authToken)
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
t.Logf("Response Body: %s", r.Body.String())
id := gjson.Get(r.Body.String(), "session_id").String()
assert.Equal(t, rnd.SessionID(authToken), id)
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("AdminAuthenticatedRequestSessionsWithID", func(t *testing.T) {
app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
GetSession(router)
authToken := AuthenticateAdmin(app, router)
t.Logf("Auth Token: %s", authToken)
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/sessions/"+rnd.SessionID(authToken), authToken)
t.Logf("Response Body: %s", r.Body.String())
id := gjson.Get(r.Body.String(), "session_id").String()
assert.Equal(t, rnd.SessionID(authToken), id)
assert.Equal(t, http.StatusOK, r.Code)
})
}
func TestDeleteSession(t *testing.T) {
@ -231,11 +261,8 @@ func TestDeleteSession(t *testing.T) {
DeleteSession(router)
authToken := AuthenticateAdmin(app, router)
// f9ae12e95a01bcc7faae6497124cd721eaf13c1dad301dbc
t.Logf("authToken: %s", authToken)
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
assert.Equal(t, http.StatusOK, r.Code)
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), "")
assert.Equal(t, http.StatusUnauthorized, r.Code)
})
t.Run("AdminAuthenticatedRequest", func(t *testing.T) {
app, router, conf := NewApiTest()
@ -245,9 +272,31 @@ func TestDeleteSession(t *testing.T) {
DeleteSession(router)
authToken := AuthenticateAdmin(app, router)
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session", authToken)
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("AdminAuthenticatedRequestWithID", func(t *testing.T) {
app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
DeleteSession(router)
authToken := AuthenticateAdmin(app, router)
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("AdminAuthenticatedRequestSessionsWithID", func(t *testing.T) {
app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
DeleteSession(router)
authToken := AuthenticateAdmin(app, router)
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/sessions/"+rnd.SessionID(authToken), authToken)
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("AdminAuthenticatedLogout", func(t *testing.T) {
app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd)

View file

@ -35,7 +35,7 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
if jsonErr := json.Unmarshal(m, &info); jsonErr != nil {
// Do nothing.
} else {
if s := Session(info.AuthToken); s != nil {
if s := Session(ws.RemoteAddr().String(), info.AuthToken); s != nil {
wsAuth.mutex.Lock()
wsAuth.sid[connId] = s.ID
wsAuth.rid[connId] = s.RefID

View file

@ -154,6 +154,11 @@ func SessionStatusForbidden() *Session {
return &Session{Status: http.StatusForbidden}
}
// SessionStatusTooManyRequests returns a session with status too many requests (429).
func SessionStatusTooManyRequests() *Session {
return &Session{Status: http.StatusTooManyRequests}
}
// FindSessionByRefID finds an existing session by ref ID.
func FindSessionByRefID(refId string) *Session {
if !rnd.IsRefID(refId) {
@ -340,9 +345,15 @@ func (m *Session) AuthInfo() string {
return fmt.Sprintf("%s (%s)", provider.Pretty(), method.Pretty())
}
// Provider returns the authentication provider.
func (m *Session) Provider() authn.ProviderType {
return authn.Provider(m.AuthProvider)
// SetAuthID sets a custom authentication identifier.
func (m *Session) SetAuthID(id string) *Session {
if id == "" {
return m
}
m.AuthID = clean.Name(id)
return m
}
// Method returns the authentication method.
@ -350,9 +361,20 @@ func (m *Session) Method() authn.MethodType {
return authn.Method(m.AuthMethod)
}
// IsClient checks whether this session is used to authenticate an API client.
func (m *Session) IsClient() bool {
return authn.Provider(m.AuthProvider).IsClient()
// SetMethod sets a custom authentication method.
func (m *Session) SetMethod(method authn.MethodType) *Session {
if method == "" {
return m
}
m.AuthMethod = method.String()
return m
}
// Provider returns the authentication provider.
func (m *Session) Provider() authn.ProviderType {
return authn.Provider(m.AuthProvider)
}
// SetProvider updates the session's authentication provider.
@ -366,6 +388,11 @@ func (m *Session) SetProvider(provider authn.ProviderType) *Session {
return m
}
// IsClient checks whether this session is used to authenticate an API client.
func (m *Session) IsClient() bool {
return authn.Provider(m.AuthProvider).IsClient()
}
// ChangePassword changes the password of the current user.
func (m *Session) ChangePassword(newPw string) (err error) {
u := m.User()
@ -465,8 +492,8 @@ func (m *Session) SetContext(c *gin.Context) *Session {
}
// Set client ip address from request context.
if ip := header.ClientIP(c); ip != "" {
m.SetClientIP(ip)
if clientIp := header.ClientIP(c); clientIp != "" {
m.SetClientIP(clientIp)
} else if m.ClientIP == "" {
// Unit tests often do not set a client IP.
m.SetClientIP(UnknownIP)
@ -489,8 +516,8 @@ func (m *Session) UpdateContext(c *gin.Context) *Session {
changed := false
// Set client ip address from request context.
if ip := header.ClientIP(c); ip != "" && (ip != m.ClientIP || m.LoginIP == "") {
m.SetClientIP(ip)
if clientIp := header.ClientIP(c); clientIp != "" && (clientIp != m.ClientIP || m.LoginIP == "") {
m.SetClientIP(clientIp)
changed = true
} else if m.ClientIP == "" {
// Unit tests often do not set a client IP.
@ -701,6 +728,8 @@ func (m *Session) Abort(c *gin.Context) bool {
switch m.Status {
case http.StatusUnauthorized:
c.AbortWithStatusJSON(m.Status, i18n.NewResponse(m.Status, i18n.ErrUnauthorized))
case http.StatusTooManyRequests:
c.AbortWithStatusJSON(m.Status, gin.H{"error": "rate limit exceeded", "code": http.StatusTooManyRequests})
default:
c.AbortWithStatusJSON(http.StatusForbidden, i18n.NewResponse(http.StatusForbidden, i18n.ErrForbidden))
}

View file

@ -50,6 +50,21 @@ var SessionFixtures = SessionMap{
UserUID: UserFixtures.Pointer("alice").UserUID,
UserName: UserFixtures.Pointer("alice").UserName,
},
"alice_token_personal": {
authToken: "bSJu9-2sr54-ZOasm-8QusP",
ID: rnd.SessionID("bSJu9-2sr54-ZOasm-8QusP"),
RefID: "sess6ey1ykya",
SessTimeout: -1,
SessExpires: UnixTime() + UnixDay,
AuthScope: clean.Scope("*"),
AuthProvider: authn.ProviderClient.String(),
AuthMethod: authn.MethodAccessToken.String(),
AuthID: "alice_token_personal",
LastActive: -1,
user: UserFixtures.Pointer("alice"),
UserUID: UserFixtures.Pointer("alice").UserUID,
UserName: UserFixtures.Pointer("alice").UserName,
},
"alice_token_webdav": {
authToken: "bHcZP-YxRbi-irKII-W1kpz",
ID: rnd.SessionID("bHcZP-YxRbi-irKII-W1kpz"),

View file

@ -7,21 +7,28 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
)
// Auth checks if the credentials are valid and returns the user and authentication provider.
var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider authn.ProviderType, err error) {
name := f.Username()
// Get username from login form.
nameName := f.Username()
user = FindUserByName(name)
err = AuthLocal(user, f, m)
// Find registered user account.
user = FindUserByName(nameName)
// Try local authentication.
provider, err = AuthLocal(user, f, m, c)
if err != nil {
return user, authn.ProviderNone, err
@ -30,60 +37,116 @@ var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider
// Update login timestamp.
user.UpdateLoginTime()
return user, authn.ProviderLocal, err
return user, provider, err
}
// AuthSession returns the client session that belongs to the auth token provided, or returns nil if it was not found.
func AuthSession(f form.Login, c *gin.Context) (sess *Session, user *User, err error) {
if f.Password == "" {
// Abort authentication if no token was provided.
return nil, nil, fmt.Errorf("no auth secret provided")
} else if !rnd.IsAuthSecret(f.Password) {
// Abort authentication if token doesn't match expected format.
return nil, nil, fmt.Errorf("auth secret does not match expected format")
}
// Get session ID for the auth token provided.
sid := rnd.SessionID(f.Password)
// Find the session based on the hashed token used as session ID and return it.
sess, err = FindSession(sid)
// Log error and return nil if no matching session was found.
if sess == nil || err != nil {
return nil, nil, fmt.Errorf("invalid auth secret")
}
// Update the client IP and the user agent from
// the request context if they have changed.
sess.UpdateContext(c)
// Returns session and user if all checks have passed.
return sess, sess.User(), nil
}
// AuthLocal authenticates against the local user database with the specified username and password.
func AuthLocal(user *User, f form.Login, m *Session) (err error) {
name := f.Username()
func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (authn.ProviderType, error) {
// Get client IP from request context.
clientIp := header.ClientIP(c)
// User found?
// Get username from login form.
userName := f.Username()
// Check if a session has been created.
if m == nil {
event.AuditErr([]string{clientIp, "login as %s", "invalid session"}, clean.LogQuote(userName))
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
}
// Check if user account exists.
if user == nil {
message := "account not found"
if m != nil {
limiter.Login.Reserve(m.IP())
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
m.Status = http.StatusUnauthorized
}
return i18n.Error(i18n.ErrInvalidCredentials)
limiter.Login.Reserve(clientIp)
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
m.Status = http.StatusUnauthorized
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
}
// Login allowed?
if !user.Provider().IsDefault() && !user.Provider().IsLocal() {
message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String())
if m != nil {
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
m.Status = http.StatusUnauthorized
}
return i18n.Error(i18n.ErrInvalidCredentials)
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
m.Status = http.StatusUnauthorized
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
} else if !user.CanLogIn() {
message := "account disabled"
if m != nil {
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
m.Status = http.StatusUnauthorized
}
return i18n.Error(i18n.ErrInvalidCredentials)
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
m.Status = http.StatusUnauthorized
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
}
// Password valid?
// Authentication with personal access token if a valid secret has been provided as password.
if authSess, authUser, err := AuthSession(f, c); err == nil {
if !authUser.IsRegistered() || authUser.UserUID != user.UserUID {
message := "incorrect user"
limiter.Login.Reserve(clientIp)
event.AuditErr([]string{clientIp, "session %s", "login as %s with auth secret", message}, m.RefID, clean.LogQuote(userName))
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
m.Status = http.StatusUnauthorized
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
} else if !authSess.IsClient() || authSess.Method() != authn.MethodAccessToken || !authSess.HasScope(acl.ResourceSessions.String()) {
message := "unauthorized"
limiter.Login.Reserve(clientIp)
event.AuditErr([]string{clientIp, "session %s", "login as %s with auth secret", message}, m.RefID, clean.LogQuote(userName))
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
m.Status = http.StatusUnauthorized
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
} else {
m.SetAuthID(authSess.AuthID)
m.SetMethod(authn.MethodSession)
event.AuditInfo([]string{clientIp, "session %s", "login as %s with auth secret", "succeeded"}, m.RefID, clean.LogQuote(userName))
event.LoginInfo(clientIp, "api", userName, m.UserAgent)
return authn.ProviderClient, err
}
}
// Otherwise, check account password.
if user.WrongPassword(f.Password) {
message := "incorrect password"
if m != nil {
limiter.Login.Reserve(m.IP())
event.AuditErr([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
m.Status = http.StatusUnauthorized
}
return i18n.Error(i18n.ErrInvalidCredentials)
limiter.Login.Reserve(clientIp)
event.AuditErr([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
m.Status = http.StatusUnauthorized
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
} else if m != nil {
event.AuditInfo([]string{m.IP(), "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(name))
event.LoginInfo(m.IP(), "api", name, m.UserAgent)
event.AuditInfo([]string{clientIp, "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(userName))
event.LoginInfo(clientIp, "api", userName, m.UserAgent)
}
return err
return authn.ProviderLocal, nil
}
// LogIn performs authentication checks against the specified login form.

View file

@ -5,78 +5,246 @@ import (
"net/http/httptest"
"testing"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/rnd"
)
func TestAuthSession(t *testing.T) {
t.Run("RandomAuthSecret", func(t *testing.T) {
// Create test request form.
f := form.Login{
UserName: "alice",
Password: rnd.AuthSecret(),
}
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
authSess, authUser, authErr := AuthSession(f, c)
assert.Nil(t, authSess)
assert.Nil(t, authUser)
assert.Error(t, authErr)
})
t.Run("RandomAuthToken", func(t *testing.T) {
// Create test request form.
f := form.Login{
UserName: "alice",
Password: rnd.AuthToken(),
}
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
authSess, authUser, authErr := AuthSession(f, c)
assert.Nil(t, authSess)
assert.Nil(t, authUser)
assert.Error(t, authErr)
})
t.Run("AliceAuthToken", func(t *testing.T) {
s := SessionFixtures.Get("alice_token")
// Create test request form.
f := form.Login{
UserName: "alice",
Password: s.AuthToken(),
}
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
authSess, authUser, authErr := AuthSession(f, c)
assert.Nil(t, authSess)
assert.Nil(t, authUser)
assert.Error(t, authErr)
})
t.Run("AliceTokenPersonal", func(t *testing.T) {
s := SessionFixtures.Get("alice_token_personal")
u := FindUserByName("alice")
// Create test request form.
f := form.Login{
UserName: "alice",
Password: s.AuthToken(),
}
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
authSess, authUser, authErr := AuthSession(f, c)
if authErr != nil {
t.Fatal(authErr)
}
assert.NotNil(t, authSess)
assert.NotNil(t, authUser)
assert.Equal(t, u.UserUID, s.UserUID)
assert.Equal(t, u.Username(), s.Username())
assert.Equal(t, authUser.UserUID, authSess.UserUID)
assert.Equal(t, authUser.Username(), authSess.Username())
assert.Equal(t, authUser.UserUID, authUser.UserUID)
assert.Equal(t, authUser.Username(), authUser.Username())
assert.True(t, authSess.IsRegistered())
assert.True(t, authSess.HasUser())
assert.True(t, authSess.HasScope(acl.ResourceWebDAV.String()))
assert.True(t, authSess.HasScope(acl.ResourceSessions.String()))
})
t.Run("AliceTokenWebdav", func(t *testing.T) {
s := SessionFixtures.Get("alice_token_webdav")
u := FindUserByName("alice")
// Create test request form.
f := form.Login{
UserName: "alice",
Password: s.AuthToken(),
}
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
authSess, authUser, authErr := AuthSession(f, c)
if authErr != nil {
t.Fatal(authErr)
}
assert.NotNil(t, authSess)
assert.NotNil(t, authUser)
assert.Equal(t, u.UserUID, s.UserUID)
assert.Equal(t, u.Username(), s.Username())
assert.Equal(t, authUser.UserUID, authSess.UserUID)
assert.Equal(t, authUser.Username(), authSess.Username())
assert.Equal(t, authUser.UserUID, authUser.UserUID)
assert.Equal(t, authUser.Username(), authUser.Username())
assert.True(t, authSess.IsRegistered())
assert.True(t, authSess.HasUser())
assert.True(t, authSess.HasScope(acl.ResourceWebDAV.String()))
assert.False(t, authSess.HasScope(acl.ResourceSessions.String()))
})
}
func TestAuthLocal(t *testing.T) {
t.Run("Alice", func(t *testing.T) {
m := FindSessionByRefID("sessxkkcabch")
u := FindUserByName("alice")
// Create test request form.
frm := form.Login{
UserName: "alice",
Password: "Alice123!",
}
if err := AuthLocal(u, frm, m); err != nil {
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
if provider, err := AuthLocal(u, frm, m, c); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, authn.ProviderLocal, provider)
}
})
t.Run("Wrong credentials", func(t *testing.T) {
m := FindSessionByRefID("sessxkkcabch")
u := FindUserByName("alice")
// Create test request form.
frm := form.Login{
UserName: "alice",
Password: "photoprism",
}
if err := AuthLocal(u, frm, m); err == nil {
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
if provider, err := AuthLocal(u, frm, m, c); err == nil {
t.Fatal("auth should fail")
} else {
assert.Equal(t, authn.ProviderNone, provider)
}
})
t.Run("No login rights", func(t *testing.T) {
m := &Session{}
u := FindUserByName("friend")
u.CanLogin = false
// Create test request form.
frm := form.Login{
UserName: "friend",
Password: "!Friend321",
}
if err := AuthLocal(u, frm, m); err == nil {
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
if provider, err := AuthLocal(u, frm, m, c); err == nil {
t.Fatal("auth should fail")
} else {
assert.Equal(t, authn.ProviderNone, provider)
}
u.CanLogin = true
})
t.Run("Authentication disabled", func(t *testing.T) {
m := &Session{}
u := FindUserByName("friend")
u.SetProvider(authn.ProviderNone)
// Create test request form.
frm := form.Login{
UserName: "friend",
Password: "!Friend321",
}
if err := AuthLocal(u, frm, m); err == nil {
// Create test request context.
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Check authentication result.
if provider, err := AuthLocal(u, frm, m, c); err == nil {
t.Fatal("auth should fail")
} else {
assert.Equal(t, authn.ProviderNone, provider)
}
u.SetProvider(authn.ProviderLocal)
@ -85,9 +253,7 @@ func TestAuthLocal(t *testing.T) {
func TestSessionLogIn(t *testing.T) {
const clientIp = "1.2.3.4"
rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
t.Run("Admin", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour*6)
@ -99,12 +265,13 @@ func TestSessionLogIn(t *testing.T) {
Password: "photoprism",
}
// Create HTTP request.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
ctx.Request.RemoteAddr = "1.2.3.4"
// Create test request context.
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in.
if err := m.LogIn(frm, ctx); err != nil {
if err := m.LogIn(frm, c); err != nil {
t.Fatal(err)
}
})
@ -118,12 +285,13 @@ func TestSessionLogIn(t *testing.T) {
Password: "wrong",
}
// Create HTTP request.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
ctx.Request.RemoteAddr = "1.2.3.4"
// Create test request context.
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in.
if err := m.LogIn(frm, ctx); err == nil {
if err := m.LogIn(frm, c); err == nil {
t.Fatal("login should fail")
}
})
@ -137,12 +305,13 @@ func TestSessionLogIn(t *testing.T) {
Password: "password",
}
// Create HTTP request.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
ctx.Request.RemoteAddr = "1.2.3.4"
// Create test request context.
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in.
if err := m.LogIn(frm, ctx); err == nil {
if err := m.LogIn(frm, c); err == nil {
t.Fatal("login should fail")
}
})
@ -155,12 +324,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfn2k",
}
// Create HTTP request.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
ctx.Request.RemoteAddr = "1.2.3.4"
// Create test request context.
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in.
if err := m.LogIn(frm, ctx); err != nil {
if err := m.LogIn(frm, c); err != nil {
t.Fatal(err)
}
})
@ -174,12 +344,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfxxx",
}
// Create HTTP request.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
ctx.Request.RemoteAddr = "1.2.3.4"
// Create test request context.
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in.
if err := m.LogIn(frm, ctx); err == nil {
if err := m.LogIn(frm, c); err == nil {
t.Fatal("login should fail")
}
})
@ -193,12 +364,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfn2k",
}
// Create HTTP request.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
ctx.Request.RemoteAddr = "1.2.3.4"
// Create test request context.
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in.
if err := m.LogIn(frm, ctx); err != nil {
if err := m.LogIn(frm, c); err != nil {
t.Fatal(err)
}
})
@ -212,12 +384,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfxxx",
}
// Create HTTP request.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
ctx.Request.RemoteAddr = "1.2.3.4"
// Create test request context.
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in.
if err := m.LogIn(frm, ctx); err == nil {
if err := m.LogIn(frm, c); err == nil {
t.Fatal("login should fail")
}
})

View file

@ -1,6 +1,7 @@
package entity
import (
"net/http"
"testing"
"time"
@ -130,13 +131,19 @@ func TestDeleteClientSessions(t *testing.T) {
func TestSessionStatusUnauthorized(t *testing.T) {
m := SessionStatusUnauthorized()
assert.Equal(t, 401, m.Status)
assert.Equal(t, http.StatusUnauthorized, m.Status)
assert.IsType(t, &Session{}, m)
}
func TestSessionStatusForbidden(t *testing.T) {
m := SessionStatusForbidden()
assert.Equal(t, 403, m.Status)
assert.Equal(t, http.StatusForbidden, m.Status)
assert.IsType(t, &Session{}, m)
}
func TestSessionStatusTooManyRequests(t *testing.T) {
m := SessionStatusTooManyRequests()
assert.Equal(t, http.StatusTooManyRequests, m.Status)
assert.IsType(t, &Session{}, m)
}

View file

@ -0,0 +1,20 @@
package limiter
import (
"time"
"golang.org/x/time/rate"
)
const (
DefaultAuthInterval = time.Second * 15 // average authentication errors per second
DefaultAuthLimit = 100 // authentication error burst rate limit
DefaultLoginInterval = time.Minute // average failed logins per second
DefaultLoginLimit = 10 // failed logins burst rate limit
)
// Auth limits the number of authentication errors from a single IP per time interval (every 15 seconds by default).
var Auth = NewLimit(rate.Every(DefaultAuthInterval), DefaultAuthLimit)
// Login limits the number of failed login attempts from a single IP per time interval (one per minute by default).
var Login = NewLimit(rate.Every(DefaultLoginInterval), DefaultLoginLimit)

View file

@ -1,13 +0,0 @@
package limiter
import (
"time"
"golang.org/x/time/rate"
)
const DefaultLoginLimit = 10
const DefaultLoginInterval = time.Minute
// Login limits failed authentication requests (one per minute).
var Login = NewLimit(rate.Every(DefaultLoginInterval), DefaultLoginLimit)

View file

@ -7,9 +7,9 @@ import (
)
// Middleware registers the IP rate limiter middleware.
func Middleware(ip *Limit) gin.HandlerFunc {
func Middleware(limiter *Limit) gin.HandlerFunc {
return func(c *gin.Context) {
if l := ip.IP(c.ClientIP()); !l.Allow() {
if l := limiter.IP(c.ClientIP()); !l.Allow() {
c.AbortWithStatus(http.StatusTooManyRequests)
return
}

View file

@ -13,6 +13,7 @@ type MethodType string
// Authentication methods.
const (
MethodDefault MethodType = "default"
MethodSession MethodType = "session"
MethodAccessToken MethodType = "access_token"
MethodOAuth2 MethodType = "oauth2"
MethodOIDC MethodType = "oidc"

View file

@ -2,6 +2,6 @@ package header
const (
CacheControl = "Cache-Control"
CacheControlNoCache = "no-cache"
CacheControlNoStore = "no-store"
CacheControlNoCache = "no-cache"
)