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. // Use a static auth token in public mode, as no additional authentication is required.
this.setAuthToken(PublicAuthToken); this.setAuthToken(PublicAuthToken);
this.setId(PublicSessionID); this.setId(PublicSessionID);
return Api.get("session/" + this.getId()).then((resp) => { return Api.get("session").then((resp) => {
this.setResp(resp); this.setResp(resp);
return Promise.resolve(); return Promise.resolve();
}); });
} else if (this.isAuthenticated()) { } else if (this.isAuthenticated()) {
// Check the auth token by fetching the client session data from the API. // 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) => { .then((resp) => {
this.setResp(resp); this.setResp(resp);
return Promise.resolve(); return Promise.resolve();
@@ -452,7 +452,7 @@ export default class Session {
logout(noRedirect) { logout(noRedirect) {
if (this.isAuthenticated()) { if (this.isAuthenticated()) {
return Api.delete("session/" + this.getId()) return Api.delete("session")
.then(() => { .then(() => {
return this.onLogout(noRedirect); 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Verstek" msgstr "Verstek"
@@ -1343,7 +1343,7 @@ msgstr "Laaste sinkronisering"
msgid "Latitude" msgid "Latitude"
msgstr "Breedtegraad" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1408,8 +1408,8 @@ msgstr "Leef"
msgid "Live Photos" msgid "Live Photos"
msgstr "Regstreekse Foto's" msgstr "Regstreekse Foto's"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Plaaslik" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Geen" msgstr "Geen"
@@ -2157,7 +2157,7 @@ msgstr "Diens-URL"
msgid "Services" msgid "Services"
msgstr "Dienste" msgstr "Dienste"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sessie" msgstr "Sessie"

View File

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

View File

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

View File

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

View File

@@ -661,8 +661,8 @@ msgstr "Registres de depuració"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Per defecte" msgstr "Per defecte"
@@ -1346,7 +1346,7 @@ msgstr "Última sincronització"
msgid "Latitude" msgid "Latitude"
msgstr "Latitud" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "En viu"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fotos en directe" msgstr "Fotos en directe"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Cap" msgstr "Cap"
@@ -2160,7 +2160,7 @@ msgstr "URL del servei"
msgid "Services" msgid "Services"
msgstr "Serveis" msgstr "Serveis"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sessió" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Výchozí" msgstr "Výchozí"
@@ -1346,7 +1346,7 @@ msgstr "Poslední synchronizace"
msgid "Latitude" msgid "Latitude"
msgstr "Zeměpisná šířka" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Živé"
msgid "Live Photos" msgid "Live Photos"
msgstr "Živé fotografie" msgstr "Živé fotografie"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Místní" 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." 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í." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Žádné" msgstr "Žádné"
@@ -2160,7 +2160,7 @@ msgstr "URL služby"
msgid "Services" msgid "Services"
msgstr "Služby" msgstr "Služby"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Relace" msgstr "Relace"

View File

@@ -661,8 +661,8 @@ msgstr "Fejlfindingslog"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Standard" msgstr "Standard"
@@ -1346,7 +1346,7 @@ msgstr "Seneste synkronisering"
msgid "Latitude" msgid "Latitude"
msgstr "Breddegrad" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Direkte"
msgid "Live Photos" msgid "Live Photos"
msgstr "Live-fotos" msgstr "Live-fotos"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokal" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Ingen" msgstr "Ingen"
@@ -2160,7 +2160,7 @@ msgstr "Service-URL"
msgid "Services" msgid "Services"
msgstr "Tjenester" msgstr "Tjenester"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Standard" msgstr "Standard"
@@ -1346,7 +1346,7 @@ msgstr "Letzte Synchronisation"
msgid "Latitude" msgid "Latitude"
msgstr "Breitengrad" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Live Photos" msgstr "Live Photos"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokal" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Keine" msgstr "Keine"
@@ -2160,7 +2160,7 @@ msgstr "Dienst-URL"
msgid "Services" msgid "Services"
msgstr "Dienste" msgstr "Dienste"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Session" msgstr "Session"

View File

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

View File

@@ -660,8 +660,8 @@ msgstr ""
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "" msgstr ""
@@ -1345,7 +1345,7 @@ msgstr ""
msgid "Latitude" msgid "Latitude"
msgstr "" msgstr ""
#: src/options/admin.js:49 src/options/admin.js:81 #: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD" msgid "LDAP/AD"
msgstr "" msgstr ""
@@ -1410,8 +1410,8 @@ msgstr ""
msgid "Live Photos" msgid "Live Photos"
msgstr "" msgstr ""
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "" msgstr ""
@@ -1657,7 +1657,7 @@ msgstr ""
msgid "Non-photographic and low-quality images require a review before they appear in search results." msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "" 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "" msgstr ""
@@ -2159,7 +2159,7 @@ msgstr ""
msgid "Services" msgid "Services"
msgstr "" msgstr ""
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Por defecto" msgstr "Por defecto"
@@ -1346,7 +1346,7 @@ msgstr "Última sincronización"
msgid "Latitude" msgid "Latitude"
msgstr "Latitud" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1412,8 +1412,8 @@ msgstr "En vivo"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fotos en vivo" msgstr "Fotos en vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Ninguno" msgstr "Ninguno"
@@ -2161,7 +2161,7 @@ msgstr "URL del servicio"
msgid "Services" msgid "Services"
msgstr "Servicios" msgstr "Servicios"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sesión" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Vaikimisi" msgstr "Vaikimisi"
@@ -1343,7 +1343,7 @@ msgstr "Viimane sünkroonimine"
msgid "Latitude" msgid "Latitude"
msgstr "Laiuskraad" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1408,8 +1408,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Liikuvad fotod" msgstr "Liikuvad fotod"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Kohalik" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Puudub" msgstr "Puudub"
@@ -2157,7 +2157,7 @@ msgstr "Teenuse URL"
msgid "Services" msgid "Services"
msgstr "Teenused" msgstr "Teenused"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sessioon" msgstr "Sessioon"

View File

@@ -658,8 +658,8 @@ msgstr "Arazte-erregistroak"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Lehenetsia" msgstr "Lehenetsia"
@@ -1343,7 +1343,7 @@ msgstr "Azken sinkronizazioa"
msgid "Latitude" msgid "Latitude"
msgstr "Latitudea" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1408,8 +1408,8 @@ msgstr "Zuzenean"
msgid "Live Photos" msgid "Live Photos"
msgstr "Zuzeneko Argazkiak" msgstr "Zuzeneko Argazkiak"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Tokikoa" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Bat ere ez" msgstr "Bat ere ez"
@@ -2157,7 +2157,7 @@ msgstr "Zerbitzuaren URLa"
msgid "Services" msgid "Services"
msgstr "Zerbitzuak" msgstr "Zerbitzuak"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Saioa" msgstr "Saioa"

View File

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

View File

@@ -658,8 +658,8 @@ msgstr "Vianmäärityslokit"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Oletus" msgstr "Oletus"
@@ -1343,7 +1343,7 @@ msgstr "Viimeisin synkronointi"
msgid "Latitude" msgid "Latitude"
msgstr "Leveysaste" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1408,8 +1408,8 @@ msgstr "Live Photo -kuva"
msgid "Live Photos" msgid "Live Photos"
msgstr "Kuvat" msgstr "Kuvat"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Paikallinen" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Ei mitään" msgstr "Ei mitään"
@@ -2157,7 +2157,7 @@ msgstr "Palvelun URL-osoite"
msgid "Services" msgid "Services"
msgstr "Palvelun URL-osoite" msgstr "Palvelun URL-osoite"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Istunto" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Valeur par défaut" msgstr "Valeur par défaut"
@@ -1346,7 +1346,7 @@ msgstr "Dernière synchro"
msgid "Latitude" msgid "Latitude"
msgstr "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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Photos en direct" msgstr "Photos en direct"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Locale" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Aucun" msgstr "Aucun"
@@ -2160,7 +2160,7 @@ msgstr "URL du service"
msgid "Services" msgid "Services"
msgstr "Services" msgstr "Services"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "ברירת מחדל" msgstr "ברירת מחדל"
@@ -1346,7 +1346,7 @@ msgstr "סנכרון אחרון"
msgid "Latitude" msgid "Latitude"
msgstr "קו רוחב" msgstr "קו רוחב"
#: src/options/admin.js:49 src/options/admin.js:81 #: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "חי"
msgid "Live Photos" msgid "Live Photos"
msgstr "תמונות חיות" msgstr "תמונות חיות"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "מְקוֹמִי" msgstr "מְקוֹמִי"
@@ -1658,7 +1658,7 @@ msgstr "אין אזהרות או שגיאות המכילות מילת מפתח
msgid "Non-photographic and low-quality images require a review before they appear in search results." msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "תמונות שאינן נראות צילום או באיכות נמוכה דורשות בדיקה לפני שהן מופיעות בתוצאות החיפוש." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "ללא" msgstr "ללא"
@@ -2160,7 +2160,7 @@ msgstr "נתיב השרות"
msgid "Services" msgid "Services"
msgstr "שירותים" msgstr "שירותים"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "מוֹשָׁב" msgstr "מוֹשָׁב"

View File

@@ -661,8 +661,8 @@ msgstr "दोषमार्जन लॉग"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "चूक" msgstr "चूक"
@@ -1346,7 +1346,7 @@ msgstr "अंतिम सिंक"
msgid "Latitude" msgid "Latitude"
msgstr "अक्षांश" msgstr "अक्षांश"
#: src/options/admin.js:49 src/options/admin.js:81 #: src/options/admin.js:49 src/options/admin.js:82
msgid "LDAP/AD" msgid "LDAP/AD"
msgstr "एलडीएपी/एडी" msgstr "एलडीएपी/एडी"
@@ -1411,8 +1411,8 @@ msgstr "लाइव"
msgid "Live Photos" msgid "Live Photos"
msgstr "लाइव तस्वीरें" msgstr "लाइव तस्वीरें"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "स्थानीय" msgstr "स्थानीय"
@@ -1658,7 +1658,7 @@ msgstr "इस कीवर्ड से कोई चेतावनी या
msgid "Non-photographic and low-quality images require a review before they appear in search results." msgid "Non-photographic and low-quality images require a review before they appear in search results."
msgstr "गैर-फोटोग्राफिक और निम्न-गुणवत्ता वाली छवियों को खोज परिणामों में प्रदर्शित होने से पहले समीक्षा की आवश्यकता होती है।" 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "कोई नहीं" msgstr "कोई नहीं"
@@ -2160,7 +2160,7 @@ msgstr "सेवा URL"
msgid "Services" msgid "Services"
msgstr "सेवाएं" msgstr "सेवाएं"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "सत्र" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Zadano" msgstr "Zadano"
@@ -1346,7 +1346,7 @@ msgstr "Zadnja sinkronizacija"
msgid "Latitude" msgid "Latitude"
msgstr "Zemljopisna širina" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Uživo"
msgid "Live Photos" msgid "Live Photos"
msgstr "Slike" msgstr "Slike"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokalni" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Nijedan" msgstr "Nijedan"
@@ -2160,7 +2160,7 @@ msgstr "URL usluge"
msgid "Services" msgid "Services"
msgstr "URL usluge" msgstr "URL usluge"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sjednica" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Alapértelmezett" msgstr "Alapértelmezett"
@@ -1345,7 +1345,7 @@ msgstr "Utolsó szinkronizálás"
msgid "Latitude" msgid "Latitude"
msgstr "Szélességi kör" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1410,8 +1410,8 @@ msgstr "Élő"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fényképek" msgstr "Fényképek"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Helyi" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Egyik sem" msgstr "Egyik sem"
@@ -2159,7 +2159,7 @@ msgstr "Szolgáltatás URL-je"
msgid "Services" msgid "Services"
msgstr "Szolgáltatások" msgstr "Szolgáltatások"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Ülés" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Bawaan" msgstr "Bawaan"
@@ -1346,7 +1346,7 @@ msgstr "Sinkronisasi Terakhir"
msgid "Latitude" msgid "Latitude"
msgstr "Lintang" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Langsung"
msgid "Live Photos" msgid "Live Photos"
msgstr "Foto" msgstr "Foto"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokal" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Tidak ada" msgstr "Tidak ada"
@@ -2160,7 +2160,7 @@ msgstr "URL Layanan"
msgid "Services" msgid "Services"
msgstr "URL Layanan" msgstr "URL Layanan"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sesi" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Predefinito" msgstr "Predefinito"
@@ -1346,7 +1346,7 @@ msgstr "Ultima sincronizzazione"
msgid "Latitude" msgid "Latitude"
msgstr "Latitudine" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Foto dal vivo" msgstr "Foto dal vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Locale" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Nessuno" msgstr "Nessuno"
@@ -2160,7 +2160,7 @@ msgstr "URL Servizio"
msgid "Services" msgid "Services"
msgstr "Servizi" msgstr "Servizi"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sessione" msgstr "Sessione"

View File

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

View File

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

View File

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

View File

@@ -661,8 +661,8 @@ msgstr "Log Nyahpepijat"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Lalai" msgstr "Lalai"
@@ -1346,7 +1346,7 @@ msgstr "Penyegerakan Terakhir"
msgid "Latitude" msgid "Latitude"
msgstr "Latitud" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Langsung"
msgid "Live Photos" msgid "Live Photos"
msgstr "Foto" msgstr "Foto"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Tempatan" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Tiada" msgstr "Tiada"
@@ -2160,7 +2160,7 @@ msgstr "URL perkhidmatan"
msgid "Services" msgid "Services"
msgstr "URL perkhidmatan" msgstr "URL perkhidmatan"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sesi" msgstr "Sesi"

View File

@@ -661,8 +661,8 @@ msgstr "Feilsøkingslogger"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Standard" msgstr "Standard"
@@ -1346,7 +1346,7 @@ msgstr "Siste synkronisering"
msgid "Latitude" msgid "Latitude"
msgstr "Breddegrad" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Direkte"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fotoer" msgstr "Fotoer"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokalt" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Ingen" msgstr "Ingen"
@@ -2160,7 +2160,7 @@ msgstr "Tjeneste-URL"
msgid "Services" msgid "Services"
msgstr "Tjenester" msgstr "Tjenester"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sesjon" msgstr "Sesjon"

View File

@@ -661,8 +661,8 @@ msgstr "Debug-logs"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Standaard" msgstr "Standaard"
@@ -1346,7 +1346,7 @@ msgstr "Laatste synchronisatie"
msgid "Latitude" msgid "Latitude"
msgstr "Breedtegraad" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Live foto's" msgstr "Live foto's"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokaal" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Geen" msgstr "Geen"
@@ -2160,7 +2160,7 @@ msgstr "Service URL"
msgid "Services" msgid "Services"
msgstr "Diensten" msgstr "Diensten"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sessie" msgstr "Sessie"

View File

@@ -661,8 +661,8 @@ msgstr "Logi debugowania"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Domyślny" msgstr "Domyślny"
@@ -1346,7 +1346,7 @@ msgstr "Ostatnia synchronizacja"
msgid "Latitude" msgid "Latitude"
msgstr "Szerokość geograficzna" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Zdjęcia na żywo" msgstr "Zdjęcia na żywo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokalnie" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Brak" msgstr "Brak"
@@ -2160,7 +2160,7 @@ msgstr "Adres URL do usługi"
msgid "Services" msgid "Services"
msgstr "Usługi" msgstr "Usługi"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sesja" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Padrão" msgstr "Padrão"
@@ -1346,7 +1346,7 @@ msgstr "Última Sincronia"
msgid "Latitude" msgid "Latitude"
msgstr "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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Ao vivo"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fotos ao vivo" msgstr "Fotos ao vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Nenhum" msgstr "Nenhum"
@@ -2160,7 +2160,7 @@ msgstr "URL do serviço"
msgid "Services" msgid "Services"
msgstr "Serviços" msgstr "Serviços"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sessão" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Padrão" msgstr "Padrão"
@@ -1346,7 +1346,7 @@ msgstr "Última Sincronia"
msgid "Latitude" msgid "Latitude"
msgstr "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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Ao vivo"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fotos ao vivo" msgstr "Fotos ao vivo"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Nenhum" msgstr "Nenhum"
@@ -2160,7 +2160,7 @@ msgstr "URL do serviço"
msgid "Services" msgid "Services"
msgstr "Serviços" msgstr "Serviços"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sessão" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Implicit" msgstr "Implicit"
@@ -1346,7 +1346,7 @@ msgstr "Ultima sincronizare"
msgid "Latitude" msgid "Latitude"
msgstr "Latitudine" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fotografii în direct" msgstr "Fotografii în direct"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Nici unul" msgstr "Nici unul"
@@ -2160,7 +2160,7 @@ msgstr "URL de serviciu"
msgid "Services" msgid "Services"
msgstr "Servicii" msgstr "Servicii"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sesiunea" msgstr "Sesiunea"

View File

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

View File

@@ -661,8 +661,8 @@ msgstr "Denníky ladenia"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Predvolená" msgstr "Predvolená"
@@ -1346,7 +1346,7 @@ msgstr "Posledná synchronizácia"
msgid "Latitude" msgid "Latitude"
msgstr "Šírka" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Živé"
msgid "Live Photos" msgid "Live Photos"
msgstr "Živé fotografie" msgstr "Živé fotografie"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Miestne stránky" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Žiadne" msgstr "Žiadne"
@@ -2160,7 +2160,7 @@ msgstr "URL Služby"
msgid "Services" msgid "Services"
msgstr "Služby" msgstr "Služby"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Zasadnutie" 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/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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Privzeto" msgstr "Privzeto"
@@ -1343,7 +1343,7 @@ msgstr "Zadnja sinhronizacija"
msgid "Latitude" msgid "Latitude"
msgstr "Zemljepisna širina" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1408,8 +1408,8 @@ msgstr "V živo"
msgid "Live Photos" msgid "Live Photos"
msgstr "Fotografije" msgstr "Fotografije"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokalni" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Ni" msgstr "Ni"
@@ -2157,7 +2157,7 @@ msgstr "URL storitve"
msgid "Services" msgid "Services"
msgstr "URL storitve" msgstr "URL storitve"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Seja" msgstr "Seja"

View File

@@ -661,8 +661,8 @@ msgstr "Felsökningsloggar"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Standard" msgstr "Standard"
@@ -1346,7 +1346,7 @@ msgstr "Senaste synkronisering"
msgid "Latitude" msgid "Latitude"
msgstr "Latitud" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Live"
msgid "Live Photos" msgid "Live Photos"
msgstr "Foton" msgstr "Foton"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Lokal" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Ingen" msgstr "Ingen"
@@ -2160,7 +2160,7 @@ msgstr "Tjänstens URL"
msgid "Services" msgid "Services"
msgstr "Tjänstens URL" msgstr "Tjänstens URL"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Sammanträde" msgstr "Sammanträde"

View File

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

View File

@@ -661,8 +661,8 @@ msgstr "Hata Kayıtları"
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100 #: 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/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:59 src/options/admin.js:60 src/options/admin.js:74
#: src/options/admin.js:92 src/options/options.js:313 #: 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 #: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
msgid "Default" msgid "Default"
msgstr "Varsayılan" msgstr "Varsayılan"
@@ -1346,7 +1346,7 @@ msgstr "Son Senkronizasyon"
msgid "Latitude" msgid "Latitude"
msgstr "Enlem" 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" msgid "LDAP/AD"
msgstr "LDAP/AD" msgstr "LDAP/AD"
@@ -1411,8 +1411,8 @@ msgstr "Canlı"
msgid "Live Photos" msgid "Live Photos"
msgstr "Canlı Fotoğraflar" msgstr "Canlı Fotoğraflar"
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77 #: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
#: src/options/admin.js:96 #: src/options/admin.js:97
msgid "Local" msgid "Local"
msgstr "Yerel" 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." 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." 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 #: src/options/options.js:293 src/options/options.js:389
msgid "None" msgid "None"
msgstr "Hiçbiri" msgstr "Hiçbiri"
@@ -2160,7 +2160,7 @@ msgstr "Hizmet URL'si"
msgid "Services" msgid "Services"
msgstr "Hizmetler" msgstr "Hizmetler"
#: src/model/session.js:98 #: src/model/session.js:98 src/options/admin.js:62
msgid "Session" msgid "Session"
msgstr "Oturum" msgstr "Oturum"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,14 @@ import (
) )
func TestACL_Allow(t *testing.T) { 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) { t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
assert.True(t, Resources.Allow(ResourcePhotos, RoleAdmin, ActionUpdate)) assert.True(t, Resources.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
}) })
@@ -124,6 +132,6 @@ func TestACL_DenyAll(t *testing.T) {
func TestACL_Resources(t *testing.T) { func TestACL_Resources(t *testing.T) {
t.Run("Resources", func(t *testing.T) { t.Run("Resources", func(t *testing.T) {
result := Resources.Resources() result := Resources.Resources()
assert.Len(t, result, 21) assert.Len(t, result, 22)
}) })
} }

View File

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

View File

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

View File

@@ -19,13 +19,13 @@ func UpdateClientConfig() {
// GET /api/v1/config // GET /api/v1/config
func GetClientConfig(router *gin.RouterGroup) { func GetClientConfig(router *gin.RouterGroup) {
router.GET("/config", func(c *gin.Context) { router.GET("/config", func(c *gin.Context) {
s := Session(AuthToken(c)) sess := Session(ClientIP(c), AuthToken(c))
conf := get.Config() conf := get.Config()
if s == nil { if sess == nil {
c.JSON(http.StatusOK, conf.ClientPublic()) c.JSON(http.StatusOK, conf.ClientPublic())
} else { } 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)) c.Header(header.ContentDisposition, fmt.Sprintf("attachment; filename=%s", fileName))
} }
// AddSessionHeader adds a session id header to the response. // AddAuthTokenHeader adds an X-Auth-Token header to the response.
func AddSessionHeader(c *gin.Context, id string) { func AddAuthTokenHeader(c *gin.Context, authToken string) {
c.Header(header.XSessionID, id) c.Header(header.XAuthToken, authToken)
} }
// AddContentTypeHeader adds a content type header to the response. // 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, Password: password,
})) }))
authToken = r.Header().Get(header.XSessionID) authToken = r.Header().Get(header.XAuthToken)
return return
} }

View File

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

View File

@@ -11,17 +11,24 @@ import (
"github.com/photoprism/photoprism/internal/get" "github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter" "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. // CreateSession creates a new client session and returns it as JSON if authentication was successful.
// //
// POST /api/v1/session // POST /api/v1/session
// POST /api/v1/sessions
func CreateSession(router *gin.RouterGroup) { 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 var f form.Login
clientIp := ClientIP(c)
if err := c.BindJSON(&f); err != nil { 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) AbortBadRequest(c)
return return
} }
@@ -40,8 +47,8 @@ func CreateSession(router *gin.RouterGroup) {
return return
} }
// Check limit for failed auth requests (max. 10 per minute). // Fail if authentication error rate limit is exceeded.
if limiter.Login.Reject(ClientIP(c)) { if clientIp != "" && (limiter.Login.Reject(clientIp) || limiter.Auth.Reject(clientIp)) {
limiter.AbortJSON(c) limiter.AbortJSON(c)
return return
} }
@@ -50,7 +57,7 @@ func CreateSession(router *gin.RouterGroup) {
var isNew bool var isNew bool
// Find existing session, if any. // Find existing session, if any.
if s := Session(AuthToken(c)); s != nil { if s := Session(clientIp, AuthToken(c)); s != nil {
// Update existing session. // Update existing session.
sess = s sess = s
} else { } else {
@@ -64,25 +71,28 @@ func CreateSession(router *gin.RouterGroup) {
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)}) c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return return
} else if sess, err = get.Session().Save(sess); err != nil { } 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)}) c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return return
} else if sess == nil { } else if sess == nil {
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)}) c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
return return
} else if isNew { } else if isNew {
event.AuditInfo([]string{ClientIP(c), "session %s", "created"}, sess.RefID) event.AuditInfo([]string{clientIp, "session %s", "created"}, sess.RefID)
} else { } 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. // Add auth token to response header.
AddSessionHeader(c, sess.AuthToken()) AddAuthTokenHeader(c, sess.AuthToken())
// Response includes user data, session data, and client config values. // Response includes user data, session data, and client config values.
response := CreateSessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess)) response := CreateSessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess))
// Return JSON response. // Return JSON response.
c.JSON(sess.HttpStatus(), 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/event"
"github.com/photoprism/photoprism/internal/get" "github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/internal/session" "github.com/photoprism/photoprism/internal/session"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )
@@ -19,8 +21,12 @@ import (
// //
// DELETE /api/v1/session // DELETE /api/v1/session
// DELETE /api/v1/session/:id // DELETE /api/v1/session/:id
// DELETE /api/v1/sessions/:id
func DeleteSession(router *gin.RouterGroup) { func DeleteSession(router *gin.RouterGroup) {
deleteSessionHandler := func(c *gin.Context) { deleteSessionHandler := func(c *gin.Context) {
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
// Abort if running in public mode. // Abort if running in public mode.
if get.Config().Public() { if get.Config().Public() {
// Return JSON response for confirmation. // Return JSON response for confirmation.
@@ -30,13 +36,23 @@ func DeleteSession(router *gin.RouterGroup) {
id := clean.ID(c.Param("id")) id := clean.ID(c.Param("id"))
// Get client IP address for logs and rate limiting checks. // Get client IP and auth token from request headers.
clientIP := ClientIP(c) 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. // 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 err != nil || sess == nil {
if clientIp != "" {
limiter.Auth.Reserve(clientIp)
}
Abort(c, http.StatusUnauthorized, i18n.ErrUnauthorized) Abort(c, http.StatusUnauthorized, i18n.ErrUnauthorized)
return return
} else if sess.Abort(c) { } else if sess.Abort(c) {
@@ -45,29 +61,29 @@ func DeleteSession(router *gin.RouterGroup) {
// Only admins may delete other sessions by ref id. // Only admins may delete other sessions by ref id.
if rnd.IsRefID(id) { if rnd.IsRefID(id) {
if !acl.Resources.AllowAll(acl.ResourceUsers, sess.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) { if !acl.Resources.AllowAll(acl.ResourceSessions, 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()) 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) Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return 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 { if sess = entity.FindSessionByRefID(id); sess == nil {
Abort(c, http.StatusNotFound, i18n.ErrNotFound) Abort(c, http.StatusNotFound, i18n.ErrNotFound)
return return
} }
} else if id != "" && sess.ID != id { } 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) Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return return
} }
// Delete session cache and database record. // Delete session cache and database record.
if err = sess.Delete(); err != nil { 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 { } else {
event.AuditDebug([]string{clientIP, "session %s", "deleted"}, sess.RefID) event.AuditDebug([]string{clientIp, "session %s", "deleted"}, sess.RefID)
} }
// Return JSON response for confirmation. // Return JSON response for confirmation.
@@ -76,4 +92,5 @@ func DeleteSession(router *gin.RouterGroup) {
router.DELETE("/session", deleteSessionHandler) router.DELETE("/session", deleteSessionHandler)
router.DELETE("/session/:id", 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/entity"
"github.com/photoprism/photoprism/internal/get" "github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/clean" "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. // GetSession returns the session data as JSON if authentication was successful.
// //
// GET /api/v1/session
// GET /api/v1/session/:id // GET /api/v1/session/:id
// GET /api/v1/sessions/:id
func GetSession(router *gin.RouterGroup) { func GetSession(router *gin.RouterGroup) {
getSessionHandler := func(c *gin.Context) { getSessionHandler := func(c *gin.Context) {
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
id := clean.ID(c.Param("id")) id := clean.ID(c.Param("id"))
// Check authentication token. // Abort if session id is provided but invalid.
if id == "" { if id != "" && !rnd.IsSessionID(id) {
// Abort if authentication token is missing or empty.
AbortBadRequest(c) AbortBadRequest(c)
return return
} }
conf := get.Config() conf := get.Config()
// Get client IP and auth token from request headers.
clientIp := ClientIP(c)
authToken := AuthToken(c) authToken := AuthToken(c)
// Skip authentication if app is running in public mode. // Skip authentication if app is running in public mode.
@@ -33,18 +43,25 @@ func GetSession(router *gin.RouterGroup) {
sess = get.Session().Public() sess = get.Session().Public()
id = sess.ID id = sess.ID
authToken = sess.AuthToken() authToken = sess.AuthToken()
} else if clientIp != "" && limiter.Auth.Reject(clientIp) {
// Fail if authentication error rate limit is exceeded.
limiter.AbortJSON(c)
return
} else { } else {
sess = Session(authToken) sess = Session(clientIp, authToken)
} }
switch { switch {
case sess == nil: case sess == nil:
if clientIp != "" {
limiter.Auth.Reserve(clientIp)
}
AbortUnauthorized(c) AbortUnauthorized(c)
return return
case sess.Expired(), sess.ID == "": case sess.Expired(), sess.ID == "":
AbortUnauthorized(c) AbortUnauthorized(c)
return return
case sess.Invalid(), sess.ID != id && !conf.Public(): case sess.Invalid(), id != "" && sess.ID != id && !conf.Public():
AbortForbidden(c) AbortForbidden(c)
return return
} }
@@ -52,8 +69,8 @@ func GetSession(router *gin.RouterGroup) {
// Update user information. // Update user information.
sess.RefreshUser() sess.RefreshUser()
// Add session id to response headers. // Add auth token to response header.
AddSessionHeader(c, authToken) AddAuthTokenHeader(c, authToken)
// Response includes user data, session data, and client config values. // Response includes user data, session data, and client config values.
response := GetSessionResponse(authToken, sess, get.Config().ClientSession(sess)) response := GetSessionResponse(authToken, sess, get.Config().ClientSession(sess))
@@ -62,5 +79,7 @@ func GetSession(router *gin.RouterGroup) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
router.GET("/session", getSessionHandler)
router.GET("/session/:id", getSessionHandler) router.GET("/session/:id", getSessionHandler)
router.GET("/sessions/:id", getSessionHandler)
} }

View File

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

View File

@@ -17,8 +17,8 @@ import (
func TestSession(t *testing.T) { func TestSession(t *testing.T) {
t.Run("Public", func(t *testing.T) { t.Run("Public", func(t *testing.T) {
sess := get.Session().Public() sess := get.Session().Public()
assert.Equal(t, sess, Session("")) assert.Equal(t, sess, Session("1.2.3.4", ""))
assert.Equal(t, sess, Session("638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0")) assert.Equal(t, sess, Session("1.2.3.4", "1234ffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
}) })
} }
@@ -213,13 +213,43 @@ func TestGetSession(t *testing.T) {
GetSession(router) GetSession(router)
authToken := AuthenticateAdmin(app, 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) r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
t.Logf("Response Body: %s", r.Body.String()) t.Logf("Response Body: %s", r.Body.String())
id := gjson.Get(r.Body.String(), "session_id").String() id := gjson.Get(r.Body.String(), "session_id").String()
assert.Equal(t, rnd.SessionID(authToken), id) assert.Equal(t, rnd.SessionID(authToken), id)
assert.Equal(t, http.StatusOK, r.Code) 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) { func TestDeleteSession(t *testing.T) {
@@ -231,11 +261,8 @@ func TestDeleteSession(t *testing.T) {
DeleteSession(router) DeleteSession(router)
authToken := AuthenticateAdmin(app, router) authToken := AuthenticateAdmin(app, router)
// f9ae12e95a01bcc7faae6497124cd721eaf13c1dad301dbc r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), "")
t.Logf("authToken: %s", authToken) assert.Equal(t, http.StatusUnauthorized, r.Code)
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
assert.Equal(t, http.StatusOK, r.Code)
}) })
t.Run("AdminAuthenticatedRequest", func(t *testing.T) { t.Run("AdminAuthenticatedRequest", func(t *testing.T) {
app, router, conf := NewApiTest() app, router, conf := NewApiTest()
@@ -245,9 +272,31 @@ func TestDeleteSession(t *testing.T) {
DeleteSession(router) DeleteSession(router)
authToken := AuthenticateAdmin(app, 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) r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
assert.Equal(t, http.StatusOK, r.Code) 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) { t.Run("AdminAuthenticatedLogout", func(t *testing.T) {
app, router, conf := NewApiTest() app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd) 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 { if jsonErr := json.Unmarshal(m, &info); jsonErr != nil {
// Do nothing. // Do nothing.
} else { } else {
if s := Session(info.AuthToken); s != nil { if s := Session(ws.RemoteAddr().String(), info.AuthToken); s != nil {
wsAuth.mutex.Lock() wsAuth.mutex.Lock()
wsAuth.sid[connId] = s.ID wsAuth.sid[connId] = s.ID
wsAuth.rid[connId] = s.RefID wsAuth.rid[connId] = s.RefID

View File

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

View File

@@ -50,6 +50,21 @@ var SessionFixtures = SessionMap{
UserUID: UserFixtures.Pointer("alice").UserUID, UserUID: UserFixtures.Pointer("alice").UserUID,
UserName: UserFixtures.Pointer("alice").UserName, 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": { "alice_token_webdav": {
authToken: "bHcZP-YxRbi-irKII-W1kpz", authToken: "bHcZP-YxRbi-irKII-W1kpz",
ID: rnd.SessionID("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/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
// Auth checks if the credentials are valid and returns the user and authentication provider. // 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) { 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) // Find registered user account.
err = AuthLocal(user, f, m) user = FindUserByName(nameName)
// Try local authentication.
provider, err = AuthLocal(user, f, m, c)
if err != nil { if err != nil {
return user, authn.ProviderNone, err 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. // Update login timestamp.
user.UpdateLoginTime() 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. // AuthLocal authenticates against the local user database with the specified username and password.
func AuthLocal(user *User, f form.Login, m *Session) (err error) { func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (authn.ProviderType, error) {
name := f.Username() // 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 { if user == nil {
message := "account not found" message := "account not found"
if m != nil { limiter.Login.Reserve(clientIp)
limiter.Login.Reserve(m.IP()) event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name)) event.LoginError(clientIp, "api", userName, m.UserAgent, message)
event.LoginError(m.IP(), "api", name, m.UserAgent, message) m.Status = http.StatusUnauthorized
m.Status = http.StatusUnauthorized return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
}
return i18n.Error(i18n.ErrInvalidCredentials)
} }
// Login allowed? // Login allowed?
if !user.Provider().IsDefault() && !user.Provider().IsLocal() { if !user.Provider().IsDefault() && !user.Provider().IsLocal() {
message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String()) message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String())
if m != nil { event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name)) event.LoginError(clientIp, "api", userName, m.UserAgent, message)
event.LoginError(m.IP(), "api", name, m.UserAgent, message) m.Status = http.StatusUnauthorized
m.Status = http.StatusUnauthorized return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
}
return i18n.Error(i18n.ErrInvalidCredentials)
} else if !user.CanLogIn() { } else if !user.CanLogIn() {
message := "account disabled" message := "account disabled"
if m != nil { event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name)) event.LoginError(clientIp, "api", userName, m.UserAgent, message)
event.LoginError(m.IP(), "api", name, m.UserAgent, message) m.Status = http.StatusUnauthorized
m.Status = http.StatusUnauthorized return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
}
return 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) { if user.WrongPassword(f.Password) {
message := "incorrect password" message := "incorrect password"
if m != nil { limiter.Login.Reserve(clientIp)
limiter.Login.Reserve(m.IP()) event.AuditErr([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
event.AuditErr([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name)) event.LoginError(clientIp, "api", userName, m.UserAgent, message)
event.LoginError(m.IP(), "api", name, m.UserAgent, message) m.Status = http.StatusUnauthorized
m.Status = http.StatusUnauthorized return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
}
return i18n.Error(i18n.ErrInvalidCredentials)
} else if m != nil { } else if m != nil {
event.AuditInfo([]string{m.IP(), "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(name)) event.AuditInfo([]string{clientIp, "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(userName))
event.LoginInfo(m.IP(), "api", name, m.UserAgent) event.LoginInfo(clientIp, "api", userName, m.UserAgent)
} }
return err return authn.ProviderLocal, nil
} }
// LogIn performs authentication checks against the specified login form. // LogIn performs authentication checks against the specified login form.

View File

@@ -5,78 +5,246 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/gin-gonic/gin" "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/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) { func TestAuthLocal(t *testing.T) {
t.Run("Alice", func(t *testing.T) { t.Run("Alice", func(t *testing.T) {
m := FindSessionByRefID("sessxkkcabch") m := FindSessionByRefID("sessxkkcabch")
u := FindUserByName("alice") u := FindUserByName("alice")
// Create test request form.
frm := form.Login{ frm := form.Login{
UserName: "alice", UserName: "alice",
Password: "Alice123!", 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) t.Fatal(err)
} else {
assert.Equal(t, authn.ProviderLocal, provider)
} }
}) })
t.Run("Wrong credentials", func(t *testing.T) { t.Run("Wrong credentials", func(t *testing.T) {
m := FindSessionByRefID("sessxkkcabch") m := FindSessionByRefID("sessxkkcabch")
u := FindUserByName("alice") u := FindUserByName("alice")
// Create test request form.
frm := form.Login{ frm := form.Login{
UserName: "alice", UserName: "alice",
Password: "photoprism", 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") t.Fatal("auth should fail")
} else {
assert.Equal(t, authn.ProviderNone, provider)
} }
}) })
t.Run("No login rights", func(t *testing.T) { t.Run("No login rights", func(t *testing.T) {
m := &Session{} m := &Session{}
u := FindUserByName("friend") u := FindUserByName("friend")
u.CanLogin = false u.CanLogin = false
// Create test request form.
frm := form.Login{ frm := form.Login{
UserName: "friend", UserName: "friend",
Password: "!Friend321", 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") t.Fatal("auth should fail")
} else {
assert.Equal(t, authn.ProviderNone, provider)
} }
u.CanLogin = true u.CanLogin = true
}) })
t.Run("Authentication disabled", func(t *testing.T) { t.Run("Authentication disabled", func(t *testing.T) {
m := &Session{} m := &Session{}
u := FindUserByName("friend") u := FindUserByName("friend")
u.SetProvider(authn.ProviderNone) u.SetProvider(authn.ProviderNone)
// Create test request form.
frm := form.Login{ frm := form.Login{
UserName: "friend", UserName: "friend",
Password: "!Friend321", 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") t.Fatal("auth should fail")
} else {
assert.Equal(t, authn.ProviderNone, provider)
} }
u.SetProvider(authn.ProviderLocal) u.SetProvider(authn.ProviderLocal)
@@ -85,9 +253,7 @@ func TestAuthLocal(t *testing.T) {
func TestSessionLogIn(t *testing.T) { func TestSessionLogIn(t *testing.T) {
const clientIp = "1.2.3.4" const clientIp = "1.2.3.4"
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
t.Run("Admin", func(t *testing.T) { t.Run("Admin", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour*6) m := NewSession(UnixDay, UnixHour*6)
@@ -99,12 +265,13 @@ func TestSessionLogIn(t *testing.T) {
Password: "photoprism", Password: "photoprism",
} }
// Create HTTP request. // Create test request context.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm)) c, _ := gin.CreateTestContext(rec)
ctx.Request.RemoteAddr = "1.2.3.4" c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in. // Try to log in.
if err := m.LogIn(frm, ctx); err != nil { if err := m.LogIn(frm, c); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
@@ -118,12 +285,13 @@ func TestSessionLogIn(t *testing.T) {
Password: "wrong", Password: "wrong",
} }
// Create HTTP request. // Create test request context.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm)) c, _ := gin.CreateTestContext(rec)
ctx.Request.RemoteAddr = "1.2.3.4" c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in. // 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") t.Fatal("login should fail")
} }
}) })
@@ -137,12 +305,13 @@ func TestSessionLogIn(t *testing.T) {
Password: "password", Password: "password",
} }
// Create HTTP request. // Create test request context.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm)) c, _ := gin.CreateTestContext(rec)
ctx.Request.RemoteAddr = "1.2.3.4" c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in. // 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") t.Fatal("login should fail")
} }
}) })
@@ -155,12 +324,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfn2k", ShareToken: "1jxf3jfn2k",
} }
// Create HTTP request. // Create test request context.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm)) c, _ := gin.CreateTestContext(rec)
ctx.Request.RemoteAddr = "1.2.3.4" c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in. // Try to log in.
if err := m.LogIn(frm, ctx); err != nil { if err := m.LogIn(frm, c); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
@@ -174,12 +344,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfxxx", ShareToken: "1jxf3jfxxx",
} }
// Create HTTP request. // Create test request context.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm)) c, _ := gin.CreateTestContext(rec)
ctx.Request.RemoteAddr = "1.2.3.4" c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in. // 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") t.Fatal("login should fail")
} }
}) })
@@ -193,12 +364,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfn2k", ShareToken: "1jxf3jfn2k",
} }
// Create HTTP request. // Create test request context.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm)) c, _ := gin.CreateTestContext(rec)
ctx.Request.RemoteAddr = "1.2.3.4" c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in. // Try to log in.
if err := m.LogIn(frm, ctx); err != nil { if err := m.LogIn(frm, c); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
@@ -212,12 +384,13 @@ func TestSessionLogIn(t *testing.T) {
ShareToken: "1jxf3jfxxx", ShareToken: "1jxf3jfxxx",
} }
// Create HTTP request. // Create test request context.
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm)) c, _ := gin.CreateTestContext(rec)
ctx.Request.RemoteAddr = "1.2.3.4" c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
c.Request.RemoteAddr = "1.2.3.4"
// Try to log in. // 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") t.Fatal("login should fail")
} }
}) })

View File

@@ -1,6 +1,7 @@
package entity package entity
import ( import (
"net/http"
"testing" "testing"
"time" "time"
@@ -130,13 +131,19 @@ func TestDeleteClientSessions(t *testing.T) {
func TestSessionStatusUnauthorized(t *testing.T) { func TestSessionStatusUnauthorized(t *testing.T) {
m := SessionStatusUnauthorized() m := SessionStatusUnauthorized()
assert.Equal(t, 401, m.Status) assert.Equal(t, http.StatusUnauthorized, m.Status)
assert.IsType(t, &Session{}, m) assert.IsType(t, &Session{}, m)
} }
func TestSessionStatusForbidden(t *testing.T) { func TestSessionStatusForbidden(t *testing.T) {
m := SessionStatusForbidden() 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) 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. // Middleware registers the IP rate limiter middleware.
func Middleware(ip *Limit) gin.HandlerFunc { func Middleware(limiter *Limit) gin.HandlerFunc {
return func(c *gin.Context) { 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) c.AbortWithStatus(http.StatusTooManyRequests)
return return
} }

View File

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

View File

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