Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
9586a9ec69
commit
fed1d8ad95
71 changed files with 930 additions and 507 deletions
|
@ -403,13 +403,13 @@ export default class Session {
|
|||
// Use a static auth token in public mode, as no additional authentication is required.
|
||||
this.setAuthToken(PublicAuthToken);
|
||||
this.setId(PublicSessionID);
|
||||
return Api.get("session/" + this.getId()).then((resp) => {
|
||||
return Api.get("session").then((resp) => {
|
||||
this.setResp(resp);
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else if (this.isAuthenticated()) {
|
||||
// Check the auth token by fetching the client session data from the API.
|
||||
return Api.get("session/" + this.getId())
|
||||
return Api.get("session")
|
||||
.then((resp) => {
|
||||
this.setResp(resp);
|
||||
return Promise.resolve();
|
||||
|
@ -452,7 +452,7 @@ export default class Session {
|
|||
|
||||
logout(noRedirect) {
|
||||
if (this.isAuthenticated()) {
|
||||
return Api.delete("session/" + this.getId())
|
||||
return Api.delete("session")
|
||||
.then(() => {
|
||||
return this.onLogout(noRedirect);
|
||||
})
|
||||
|
|
|
@ -658,8 +658,8 @@ msgstr "Ontfout logs"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Verstek"
|
||||
|
@ -1343,7 +1343,7 @@ msgstr "Laaste sinkronisering"
|
|||
msgid "Latitude"
|
||||
msgstr "Breedtegraad"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1408,8 +1408,8 @@ msgstr "Leef"
|
|||
msgid "Live Photos"
|
||||
msgstr "Regstreekse Foto's"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Plaaslik"
|
||||
|
||||
|
@ -1655,7 +1655,7 @@ msgstr "Geen waarskuwings of foute wat hierdie sleutelwoord bevat nie. Let daaro
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Nie-fotografiese en lae kwaliteit prente vereis 'n hersiening voordat dit in soekresultate verskyn."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Geen"
|
||||
|
@ -2157,7 +2157,7 @@ msgstr "Diens-URL"
|
|||
msgid "Services"
|
||||
msgstr "Dienste"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sessie"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "سجلات التصحيح"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "تقصير"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "آخر مزامنة"
|
|||
msgid "Latitude"
|
||||
msgstr "خط العرض"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP / AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "يعيش"
|
|||
msgid "Live Photos"
|
||||
msgstr "Live Photos"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "محلي"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "لا تحذيرات أو خطأ يحتوي على هذه الكلمة ا
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "تتطلب الصور غير الفوتوغرافية وذات الجودة المنخفضة المراجعة قبل ظهورها في نتائج البحث."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "لا أحد"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL الخدمة"
|
|||
msgid "Services"
|
||||
msgstr "خدمات"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "حصة"
|
||||
|
||||
|
|
|
@ -658,8 +658,8 @@ msgstr "Журналы адладкі"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Па змаўчанні"
|
||||
|
@ -1343,7 +1343,7 @@ msgstr "Апошняя сінхранізацыя"
|
|||
msgid "Latitude"
|
||||
msgstr "Шырата"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1408,8 +1408,8 @@ msgstr "жыць"
|
|||
msgid "Live Photos"
|
||||
msgstr "Жывыя фатаграфіі"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Мясцовы"
|
||||
|
||||
|
@ -1655,7 +1655,7 @@ msgstr "Няма папярэджанняў або памылак з гэтым
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Нефатаграфічныя і нізкаякасныя выявы патрабуюць праверкі, перш чым яны з'явяцца ў выніках пошуку."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Няма"
|
||||
|
@ -2157,7 +2157,7 @@ msgstr "URL службы"
|
|||
msgid "Services"
|
||||
msgstr "Паслугі"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "сесія"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Протоколи за отработване"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "По подразбиране"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Синхронизиране"
|
|||
msgid "Latitude"
|
||||
msgstr "Географска ширина"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "На живо"
|
|||
msgid "Live Photos"
|
||||
msgstr "Снимки"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Местни"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Няма предупреждения или грешки, съдърж
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Нефотографските изображения и изображенията с ниско качество изискват преглед, преди да се появят в резултатите от търсенето."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Няма"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL адрес на услугата"
|
|||
msgid "Services"
|
||||
msgstr "URL адрес на услугата"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Сесия"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Registres de depuració"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Per defecte"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Última sincronització"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitud"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "En viu"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fotos en directe"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Local"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "No hi ha cap advertiment ni error que contingui aquesta paraula clau. Ti
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Les imatges no fotogràfiques i de baixa qualitat requereixen una revisió abans que apareguin als resultats de la cerca."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Cap"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL del servei"
|
|||
msgid "Services"
|
||||
msgstr "Serveis"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sessió"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Protokoly ladění"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Výchozí"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Poslední synchronizace"
|
|||
msgid "Latitude"
|
||||
msgstr "Zeměpisná šířka"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Živé"
|
|||
msgid "Live Photos"
|
||||
msgstr "Živé fotografie"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Místní"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Žádná varování nebo chyba obsahující toto klíčové slovo. Mějt
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Nefotografické obrázky a snímky nízké kvality vyžadují kontrolu, než se objeví ve výsledcích vyhledávání."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Žádné"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL služby"
|
|||
msgid "Services"
|
||||
msgstr "Služby"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Relace"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Fejlfindingslog"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Standard"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Seneste synkronisering"
|
|||
msgid "Latitude"
|
||||
msgstr "Breddegrad"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Direkte"
|
|||
msgid "Live Photos"
|
||||
msgstr "Live-fotos"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokal"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Ingen advarsler eller fejl, der indeholder dette nøgleord. Bemærk, at
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Ikke-fotografiske billeder af lav kvalitet kræver en gennemgang, før de vises i søgeresultaterne."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Service-URL"
|
|||
msgid "Services"
|
||||
msgstr "Tjenester"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Session"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Debug Logs"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Standard"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Letzte Synchronisation"
|
|||
msgid "Latitude"
|
||||
msgstr "Breitengrad"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Live Photos"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokal"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Keine Warnungen oder Fehler mit diesem Suchbegriff. Bei der Suche wird z
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Nicht-fotografische Inhalte oder Bilder mit geringer Qualität werden erst nach einer Bestätigung in der Suche angezeigt."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Dienst-URL"
|
|||
msgid "Services"
|
||||
msgstr "Dienste"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Session"
|
||||
|
||||
|
|
|
@ -658,8 +658,8 @@ msgstr "Αρχεία καταγραφής σφαλμάτων"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Προεπιλογή"
|
||||
|
@ -1343,7 +1343,7 @@ msgstr "Τελευταίος συγχρονισμός"
|
|||
msgid "Latitude"
|
||||
msgstr "Γεωγραφικό πλάτος"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1408,8 +1408,8 @@ msgstr "Ζωντανό"
|
|||
msgid "Live Photos"
|
||||
msgstr "Φωτογραφίες"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Τοπικό"
|
||||
|
||||
|
@ -1655,7 +1655,7 @@ msgstr "Δεν υπάρχουν προειδοποιήσεις ή σφάλματ
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Οι μη φωτογραφικές εικόνες και οι εικόνες χαμηλής ποιότητας απαιτούν επανεξέταση προτού εμφανιστούν στα αποτελέσματα αναζήτησης."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Κανένα"
|
||||
|
@ -2157,7 +2157,7 @@ msgstr "URL υπηρεσίας"
|
|||
msgid "Services"
|
||||
msgstr "URL υπηρεσίας"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Σύνοδος"
|
||||
|
||||
|
|
|
@ -660,8 +660,8 @@ msgstr ""
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
@ -1345,7 +1345,7 @@ msgstr ""
|
|||
msgid "Latitude"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1410,8 +1410,8 @@ msgstr ""
|
|||
msgid "Live Photos"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1657,7 +1657,7 @@ msgstr ""
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr ""
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
@ -2159,7 +2159,7 @@ msgstr ""
|
|||
msgid "Services"
|
||||
msgstr ""
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Registros de depuración"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Por defecto"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Última sincronización"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitud"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1412,8 +1412,8 @@ msgstr "En vivo"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fotos en vivo"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Local"
|
||||
|
||||
|
@ -1659,7 +1659,7 @@ msgstr "No hay advertencias ni errores que contengan esta palabra clave. Tenga e
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Las imágenes no fotográficas y de baja calidad requieren una revisión antes que aparezcan en los resultados de la búsqueda."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Ninguno"
|
||||
|
@ -2161,7 +2161,7 @@ msgstr "URL del servicio"
|
|||
msgid "Services"
|
||||
msgstr "Servicios"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sesión"
|
||||
|
||||
|
|
|
@ -658,8 +658,8 @@ msgstr "Tõrkeotsingu logid"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Vaikimisi"
|
||||
|
@ -1343,7 +1343,7 @@ msgstr "Viimane sünkroonimine"
|
|||
msgid "Latitude"
|
||||
msgstr "Laiuskraad"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1408,8 +1408,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Liikuvad fotod"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Kohalik"
|
||||
|
||||
|
@ -1655,7 +1655,7 @@ msgstr "Seda märksõna sisaldavaid hoiatusi või vigu ei ole. Pane tähele, et
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Mittefotograafilised ja madala kvaliteediga pildid tuleb üle vaadata, enne kui nad otsingutulemustes ilmuvad."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Puudub"
|
||||
|
@ -2157,7 +2157,7 @@ msgstr "Teenuse URL"
|
|||
msgid "Services"
|
||||
msgstr "Teenused"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sessioon"
|
||||
|
||||
|
|
|
@ -658,8 +658,8 @@ msgstr "Arazte-erregistroak"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Lehenetsia"
|
||||
|
@ -1343,7 +1343,7 @@ msgstr "Azken sinkronizazioa"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitudea"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1408,8 +1408,8 @@ msgstr "Zuzenean"
|
|||
msgid "Live Photos"
|
||||
msgstr "Zuzeneko Argazkiak"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Tokikoa"
|
||||
|
||||
|
@ -1655,7 +1655,7 @@ msgstr "Ez dago gako-hitz hau duen abisurik edo errorerik. Kontuan izan bilaketa
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Argazkiak ez diren eta kalitate baxuko irudiak berrikusi behar dira bilaketa-emaitzetan agertu aurretik."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Bat ere ez"
|
||||
|
@ -2157,7 +2157,7 @@ msgstr "Zerbitzuaren URLa"
|
|||
msgid "Services"
|
||||
msgstr "Zerbitzuak"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Saioa"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "گزارشهای اشکال زدایی"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "پیشفرض"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "آخرین همگام سازی"
|
|||
msgid "Latitude"
|
||||
msgstr "عرض جغرافیایی"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "زنده"
|
|||
msgid "Live Photos"
|
||||
msgstr "تصاویر"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "محلی"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "هیچ هشدار یا خطایی حاوی این کلمه کلیدی ن
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "تصاویر غیرعکاسی و با کیفیت پایین قبل از اینکه در نتایج جستجو ظاهر شوند نیاز به بررسی دارند."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "هیچ یک"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL سرویس"
|
|||
msgid "Services"
|
||||
msgstr "URL سرویس"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "جلسه"
|
||||
|
||||
|
|
|
@ -658,8 +658,8 @@ msgstr "Vianmäärityslokit"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Oletus"
|
||||
|
@ -1343,7 +1343,7 @@ msgstr "Viimeisin synkronointi"
|
|||
msgid "Latitude"
|
||||
msgstr "Leveysaste"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1408,8 +1408,8 @@ msgstr "Live Photo -kuva"
|
|||
msgid "Live Photos"
|
||||
msgstr "Kuvat"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Paikallinen"
|
||||
|
||||
|
@ -1655,7 +1655,7 @@ msgstr "Ei varoituksia tai virheitä, jotka sisältävät tämän avainsanan. Hu
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Muut kuin valokuvat ja heikkolaatuiset kuvat edellyttävät tarkistusta, ennen kuin ne näkyvät hakutuloksissa."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Ei mitään"
|
||||
|
@ -2157,7 +2157,7 @@ msgstr "Palvelun URL-osoite"
|
|||
msgid "Services"
|
||||
msgstr "Palvelun URL-osoite"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Istunto"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Journaux de débogage"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Valeur par défaut"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Dernière synchro"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitude"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Photos en direct"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Locale"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Aucun avertissement ou erreur contenant ce mot-clé. Notez que la recher
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Les images non photographiques ou de mauvaise qualité doivent faire l'objet d'un examen avant d'apparaître dans les résultats de recherche."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Aucun"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL du service"
|
|||
msgid "Services"
|
||||
msgstr "Services"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Session"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Debug Logs"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "ברירת מחדל"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "סנכרון אחרון"
|
|||
msgid "Latitude"
|
||||
msgstr "קו רוחב"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "חי"
|
|||
msgid "Live Photos"
|
||||
msgstr "תמונות חיות"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "מְקוֹמִי"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "אין אזהרות או שגיאות המכילות מילת מפתח
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "תמונות שאינן נראות צילום או באיכות נמוכה דורשות בדיקה לפני שהן מופיעות בתוצאות החיפוש."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "ללא"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "נתיב השרות"
|
|||
msgid "Services"
|
||||
msgstr "שירותים"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "מוֹשָׁב"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "दोषमार्जन लॉग"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "चूक"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "अंतिम सिंक"
|
|||
msgid "Latitude"
|
||||
msgstr "अक्षांश"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "एलडीएपी/एडी"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "लाइव"
|
|||
msgid "Live Photos"
|
||||
msgstr "लाइव तस्वीरें"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "स्थानीय"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "इस कीवर्ड से कोई चेतावनी या
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "गैर-फोटोग्राफिक और निम्न-गुणवत्ता वाली छवियों को खोज परिणामों में प्रदर्शित होने से पहले समीक्षा की आवश्यकता होती है।"
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "कोई नहीं"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "सेवा URL"
|
|||
msgid "Services"
|
||||
msgstr "सेवाएं"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "सत्र"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Zapisnici otklanjanja pogrešaka"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Zadano"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Zadnja sinkronizacija"
|
|||
msgid "Latitude"
|
||||
msgstr "Zemljopisna širina"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Uživo"
|
|||
msgid "Live Photos"
|
||||
msgstr "Slike"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokalni"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Nema upozorenja ili pogreške koje sadrže ovu ključnu riječ. Imajte n
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Nefotografske slike i slike niske kvalitete zahtijevaju pregled prije nego što se pojave u rezultatima pretraživanja."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Nijedan"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL usluge"
|
|||
msgid "Services"
|
||||
msgstr "URL usluge"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sjednica"
|
||||
|
||||
|
|
|
@ -660,8 +660,8 @@ msgstr "Hibakeresési naplók"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Alapértelmezett"
|
||||
|
@ -1345,7 +1345,7 @@ msgstr "Utolsó szinkronizálás"
|
|||
msgid "Latitude"
|
||||
msgstr "Szélességi kör"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1410,8 +1410,8 @@ msgstr "Élő"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fényképek"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Helyi"
|
||||
|
||||
|
@ -1657,7 +1657,7 @@ msgstr "Nincsenek figyelmeztetések vagy hibák, amelyek ezt a kulcsszót tartal
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "A rossz minőségű képek ellenörzésre kerülnek, mielőtt megjelennének a keresési eredmények között."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Egyik sem"
|
||||
|
@ -2159,7 +2159,7 @@ msgstr "Szolgáltatás URL-je"
|
|||
msgid "Services"
|
||||
msgstr "Szolgáltatások"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Ülés"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Log Debug"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Bawaan"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Sinkronisasi Terakhir"
|
|||
msgid "Latitude"
|
||||
msgstr "Lintang"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Langsung"
|
|||
msgid "Live Photos"
|
||||
msgstr "Foto"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokal"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Tidak ada peringatan atau kesalahan yang mengandung kata kunci ini. Perh
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Gambar non-fotografis dan berkualitas rendah memerlukan peninjauan sebelum muncul di hasil pencarian."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Tidak ada"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL Layanan"
|
|||
msgid "Services"
|
||||
msgstr "URL Layanan"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sesi"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Registri di debug"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Predefinito"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Ultima sincronizzazione"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitudine"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Foto dal vivo"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Locale"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Nessun warning o errore contiene questa parola chiave. Tieni presente ch
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Le immagini non fotografiche e di bassa qualità richiedono una revisione prima di essere visualizzate nei risultati di ricerca."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Nessuno"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL Servizio"
|
|||
msgid "Services"
|
||||
msgstr "Servizi"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sessione"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "デバッグログ"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "既定"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "最終同期"
|
|||
msgid "Latitude"
|
||||
msgstr "緯度"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "ライブ"
|
|||
msgid "Live Photos"
|
||||
msgstr "ライブ写真"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "ローカル"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "このキーワードを含む警告やエラーは1つも見つかり
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "写真ではないものや、低品質な画像は検索結果に現れる前にレビューが必要です。"
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "なし"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "サービス URL"
|
|||
msgid "Services"
|
||||
msgstr "サービス"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "セッション"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "디버그 로그"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "기본값"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "마지막 동기화"
|
|||
msgid "Latitude"
|
||||
msgstr "위도"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "라이브"
|
|||
msgid "Live Photos"
|
||||
msgstr "라이브 포토"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "로컬"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "이 키워드를 포함하는 경고 또는 오류가 없습니다. 검
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "사진이 아닌 저품질 이미지는 검색 결과에 표시되기 전에 검토가 필요합니다."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "없음"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "서비스 URL"
|
|||
msgid "Services"
|
||||
msgstr "서비스"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "세션"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "تۆماری هەڵەکان"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "بنهڕهت"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "هاوکاتگەری"
|
|||
msgid "Latitude"
|
||||
msgstr "هێڵی پانیی"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "زیندوو"
|
|||
msgid "Live Photos"
|
||||
msgstr "وێنەکان"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Herêmî"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "هیچ ئاگادارییەک یان هەڵەیەک نیە کە ئەم
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "وێنە نافۆتۆگرافی و کوالێتی نزمەکان پێویستی بە پێداچونەوە هەیە پێش ئەوەی لە ئەنجامی گەڕاندا دەرکەون."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "هیچ"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "بەستەری خزمەتگوزاری"
|
|||
msgid "Services"
|
||||
msgstr "بەستەری خزمەتگوزاری"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Rûniştinî"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Derinimo žurnalai"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Numatytoji"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Sinchronizavimas"
|
|||
msgid "Latitude"
|
||||
msgstr "Platuma"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Gyvai"
|
|||
msgid "Live Photos"
|
||||
msgstr "Nuotraukos"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Vietinis"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Jokių įspėjimų ar klaidų su šiuo raktažodžiu nėra. Atkreipkite
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Prieš rodant nefotografuotus ir prastos kokybės vaizdus paieškos rezultatuose, juos reikia peržiūrėti."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Nėra"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Paslaugos URL"
|
|||
msgid "Services"
|
||||
msgstr "Paslaugos URL"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sesija"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Log Nyahpepijat"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Lalai"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Penyegerakan Terakhir"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitud"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Langsung"
|
|||
msgid "Live Photos"
|
||||
msgstr "Foto"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Tempatan"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Tiada amaran atau ralat yang mengandungi kata kunci ini. Ambil perhatian
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Imej bukan fotografi dan berkualiti rendah memerlukan semakan sebelum ia muncul dalam hasil carian."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Tiada"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL perkhidmatan"
|
|||
msgid "Services"
|
||||
msgstr "URL perkhidmatan"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sesi"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Feilsøkingslogger"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Standard"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Siste synkronisering"
|
|||
msgid "Latitude"
|
||||
msgstr "Breddegrad"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Direkte"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fotoer"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokalt"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Ingen advarsler eller feilmeldinger inneholder dette nøkkelordet. Merk
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Bilder som ikke er fotografiske eller har lav kvalitet må gjennomgås før de kommer i søkeresultater."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Tjeneste-URL"
|
|||
msgid "Services"
|
||||
msgstr "Tjenester"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sesjon"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Debug-logs"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Standaard"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Laatste synchronisatie"
|
|||
msgid "Latitude"
|
||||
msgstr "Breedtegraad"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Live foto's"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokaal"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Geen waarschuwingen of fouten met dit trefwoord. Let op: zoeken is hoofd
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Niet-fotografische beelden en beelden van lage kwaliteit moeten worden beoordeeld voordat ze in de zoekresultaten verschijnen."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Geen"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Service URL"
|
|||
msgid "Services"
|
||||
msgstr "Diensten"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sessie"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Logi debugowania"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Domyślny"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Ostatnia synchronizacja"
|
|||
msgid "Latitude"
|
||||
msgstr "Szerokość geograficzna"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Zdjęcia na żywo"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokalnie"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Brak ostrzeżeń lub błędów zawierających to słowo kluczowe. Zwró
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Obrazy niebędące fotografiami lub posiadające niską jakość wymagają zatwierdzenia, zanim pojawią się w wynikach wyszukiwania."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Brak"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Adres URL do usługi"
|
|||
msgid "Services"
|
||||
msgstr "Usługi"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sesja"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Registros de depuração"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Padrão"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Última Sincronia"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitude"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Ao vivo"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fotos ao vivo"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Local"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Nenhum alerta ou erro contendo esta palavra-chave. Note que a pesquisa d
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Imagens de baixa qualidade ou não-fotográficas necessitam de revisão antes de aparecerem nos resultados da pesquisa."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Nenhum"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL do serviço"
|
|||
msgid "Services"
|
||||
msgstr "Serviços"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sessão"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Registros de depuração"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Padrão"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Última Sincronia"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitude"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Ao vivo"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fotos ao vivo"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Local"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Nenhum alerta ou erro contento esta palavra-chave. Note que a busca dife
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Imagens de baixa qualidade ou não-fotográficas necessitam de revisão antes de aparecerem nos resultados de busca."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Nenhum"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL do serviço"
|
|||
msgid "Services"
|
||||
msgstr "Serviços"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sessão"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Jurnalele de depanare"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Implicit"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Ultima sincronizare"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitudine"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fotografii în direct"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Local"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Nu există avertismente sau erori care să conțină acest cuvânt cheie
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Imaginile nefotografice și de slabă calitate necesită o revizuire înainte de a apărea în rezultatele căutării."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Nici unul"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL de serviciu"
|
|||
msgid "Services"
|
||||
msgstr "Servicii"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sesiunea"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Отладочные Логи"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "По умолчанию"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Последняя синхронизация"
|
|||
msgid "Latitude"
|
||||
msgstr "Широта"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Прямой эфир"
|
|||
msgid "Live Photos"
|
||||
msgstr "Живые фотографии"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Местный"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Нет предупреждений или ошибок содержащ
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Файлы, не являющиеся фотографиями, или изображения низкого качества нужно одобрить, чтобы они появились в результатах поиска."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Ничего"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL сервиса"
|
|||
msgid "Services"
|
||||
msgstr "Сервисы"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Сессия"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Denníky ladenia"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Predvolená"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Posledná synchronizácia"
|
|||
msgid "Latitude"
|
||||
msgstr "Šírka"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Živé"
|
|||
msgid "Live Photos"
|
||||
msgstr "Živé fotografie"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Miestne stránky"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Nenašli sa žiadne upozornenia ani chyby ktoré by obsahovali toto kľ
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Nefotografické a fotografie nízkej kvality vyžadujú skontrolovanie pred tým než sa zobrazia vo výsledkoch vyhľadávania."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Žiadne"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL Služby"
|
|||
msgid "Services"
|
||||
msgstr "Služby"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Zasadnutie"
|
||||
|
||||
|
|
|
@ -658,8 +658,8 @@ msgstr "Dnevniki za odpravljanje napak"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Privzeto"
|
||||
|
@ -1343,7 +1343,7 @@ msgstr "Zadnja sinhronizacija"
|
|||
msgid "Latitude"
|
||||
msgstr "Zemljepisna širina"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1408,8 +1408,8 @@ msgstr "V živo"
|
|||
msgid "Live Photos"
|
||||
msgstr "Fotografije"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokalni"
|
||||
|
||||
|
@ -1655,7 +1655,7 @@ msgstr "Ni opozoril ali napak, ki bi vsebovale to ključno besedo. Upoštevajte,
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Nefotografske slike in slike nizke kakovosti je treba pred prikazom v rezultatih iskanja pregledati."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Ni"
|
||||
|
@ -2157,7 +2157,7 @@ msgstr "URL storitve"
|
|||
msgid "Services"
|
||||
msgstr "URL storitve"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Seja"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Felsökningsloggar"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Standard"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Senaste synkronisering"
|
|||
msgid "Latitude"
|
||||
msgstr "Latitud"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live"
|
|||
msgid "Live Photos"
|
||||
msgstr "Foton"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Lokal"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Inga varningar eller fel som innehåller detta nyckelord. Observera att
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Bilder som inte är fotografiska eller av låg kvalitet måste granskas innan de visas i sökresultaten."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Tjänstens URL"
|
|||
msgid "Services"
|
||||
msgstr "Tjänstens URL"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Sammanträde"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "บันทึกการดีบัก"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "ค่าเริ่มต้น"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "ซิงค์ล่าสุด"
|
|||
msgid "Latitude"
|
||||
msgstr "ละติจูด"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "แอลดีเอพี/ค.ศ"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "สด"
|
|||
msgid "Live Photos"
|
||||
msgstr "ภาพถ่าย"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "ท้องถิ่น"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "ไม่มีคำเตือนหรือข้อผิดพล
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "รูปภาพที่ไม่ใช่ภาพถ่ายและคุณภาพต่ำต้องได้รับการตรวจสอบก่อนที่จะปรากฏในผลการค้นหา"
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "ไม่มี"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL บริการ"
|
|||
msgid "Services"
|
||||
msgstr "URL บริการ"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "การประชุม"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Hata Kayıtları"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "Varsayılan"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Son Senkronizasyon"
|
|||
msgid "Latitude"
|
||||
msgstr "Enlem"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Canlı"
|
|||
msgid "Live Photos"
|
||||
msgstr "Canlı Fotoğraflar"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Yerel"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Bu anahtar kelimeyi içeren uyarı veya hata yok. Aramanın büyük/kü
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Fotoğrafik olmayan ve düşük kaliteli görseller, arama sonuçlarında görünmeden önce bir inceleme gerektirir."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Hiçbiri"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "Hizmet URL'si"
|
|||
msgid "Services"
|
||||
msgstr "Hizmetler"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Oturum"
|
||||
|
||||
|
|
|
@ -773,8 +773,8 @@ msgstr ""
|
|||
#: src/options/admin.js:45
|
||||
#: src/options/admin.js:59
|
||||
#: src/options/admin.js:60
|
||||
#: src/options/admin.js:73
|
||||
#: src/options/admin.js:92
|
||||
#: src/options/admin.js:74
|
||||
#: src/options/admin.js:93
|
||||
#: src/options/options.js:313
|
||||
#: src/options/options.js:377
|
||||
#: src/options/themes.js:492
|
||||
|
@ -1579,7 +1579,7 @@ msgid "Latitude"
|
|||
msgstr ""
|
||||
|
||||
#: src/options/admin.js:49
|
||||
#: src/options/admin.js:81
|
||||
#: src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1664,8 +1664,8 @@ msgstr ""
|
|||
|
||||
#: src/options/admin.js:46
|
||||
#: src/options/admin.js:48
|
||||
#: src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1977,8 +1977,8 @@ msgid "Non-photographic and low-quality images require a review before they appe
|
|||
msgstr ""
|
||||
|
||||
#: src/options/admin.js:52
|
||||
#: src/options/admin.js:85
|
||||
#: src/options/admin.js:100
|
||||
#: src/options/admin.js:86
|
||||
#: src/options/admin.js:101
|
||||
#: src/options/options.js:293
|
||||
#: src/options/options.js:389
|
||||
msgid "None"
|
||||
|
@ -2564,6 +2564,7 @@ msgid "Services"
|
|||
msgstr ""
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "Журнали налагодження"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "За замовчуванням"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "Остання синхронізація"
|
|||
msgid "Latitude"
|
||||
msgstr "Широта"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "Live фото"
|
|||
msgid "Live Photos"
|
||||
msgstr "Живі фото"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "Місцевий"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "Немає попереджень або помилок із цим кл
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "Нефотографічні та низькоякісні зображення потребують перевірки, перш ніж з’являться в результатах пошуку."
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "Жодного"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "URL служби"
|
|||
msgid "Services"
|
||||
msgstr "Послуги"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "Сесія"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "调试日志"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "默认"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "上次同步"
|
|||
msgid "Latitude"
|
||||
msgstr "纬度"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "实况"
|
|||
msgid "Live Photos"
|
||||
msgstr "现场照片"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "当地"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "没有包含此关键字的警告或错误,请注意,搜索区分大
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "非照片和低质量图像出现在搜索结果中前需要进行审查。"
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
|
@ -2162,7 +2162,7 @@ msgstr "服务 URL"
|
|||
msgid "Services"
|
||||
msgstr "服务"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "会议"
|
||||
|
||||
|
|
|
@ -661,8 +661,8 @@ msgstr "除錯紀錄"
|
|||
|
||||
#: src/page/admin/sessions.vue:63 src/page/admin/users.vue:100
|
||||
#: src/model/session.js:61 src/options/admin.js:44 src/options/admin.js:45
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:73
|
||||
#: src/options/admin.js:92 src/options/options.js:313
|
||||
#: src/options/admin.js:59 src/options/admin.js:60 src/options/admin.js:74
|
||||
#: src/options/admin.js:93 src/options/options.js:313
|
||||
#: src/options/options.js:377 src/options/themes.js:492 src/page/places.vue:142
|
||||
msgid "Default"
|
||||
msgstr "預設"
|
||||
|
@ -1346,7 +1346,7 @@ msgstr "上次同步"
|
|||
msgid "Latitude"
|
||||
msgstr "緯度"
|
||||
|
||||
#: src/options/admin.js:49 src/options/admin.js:81
|
||||
#: src/options/admin.js:49 src/options/admin.js:82
|
||||
msgid "LDAP/AD"
|
||||
msgstr "LDAP/AD"
|
||||
|
||||
|
@ -1411,8 +1411,8 @@ msgstr "即時"
|
|||
msgid "Live Photos"
|
||||
msgstr "原況照片"
|
||||
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:77
|
||||
#: src/options/admin.js:96
|
||||
#: src/options/admin.js:46 src/options/admin.js:48 src/options/admin.js:78
|
||||
#: src/options/admin.js:97
|
||||
msgid "Local"
|
||||
msgstr "当地"
|
||||
|
||||
|
@ -1658,7 +1658,7 @@ msgstr "沒有包含此關鍵字的警告或錯誤。請注意,搜尋區分大
|
|||
msgid "Non-photographic and low-quality images require a review before they appear in search results."
|
||||
msgstr "非照片和低品質圖像需要進行手動確認,才會出現在搜尋結果中。"
|
||||
|
||||
#: src/options/admin.js:52 src/options/admin.js:85 src/options/admin.js:100
|
||||
#: src/options/admin.js:52 src/options/admin.js:86 src/options/admin.js:101
|
||||
#: src/options/options.js:293 src/options/options.js:389
|
||||
msgid "None"
|
||||
msgstr "無"
|
||||
|
@ -2160,7 +2160,7 @@ msgstr "服務 URL"
|
|||
msgid "Services"
|
||||
msgstr "服務"
|
||||
|
||||
#: src/model/session.js:98
|
||||
#: src/model/session.js:98 src/options/admin.js:62
|
||||
msgid "Session"
|
||||
msgstr "工作階段"
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ export class Session extends RestModel {
|
|||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
return "session";
|
||||
return "sessions";
|
||||
}
|
||||
|
||||
static getModelName() {
|
||||
|
|
|
@ -59,6 +59,7 @@ export const AuthMethods = () => {
|
|||
"": $gettext("Default"),
|
||||
default: $gettext("Default"),
|
||||
access_token: $gettext("Access Token"),
|
||||
session: $gettext("Session"),
|
||||
"2fa": "2FA",
|
||||
oauth2: "OAuth2",
|
||||
oidc: "OIDC",
|
||||
|
|
|
@ -7,6 +7,14 @@ import (
|
|||
)
|
||||
|
||||
func TestACL_Allow(t *testing.T) {
|
||||
t.Run("ResourceSessions", func(t *testing.T) {
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleAdmin, AccessAll))
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleAdmin, AccessOwn))
|
||||
assert.False(t, Resources.Allow(ResourceSessions, RoleVisitor, AccessAll))
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleVisitor, AccessOwn))
|
||||
assert.False(t, Resources.Allow(ResourceSessions, RoleClient, AccessAll))
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleClient, AccessOwn))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
|
||||
assert.True(t, Resources.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
|
||||
})
|
||||
|
@ -124,6 +132,6 @@ func TestACL_DenyAll(t *testing.T) {
|
|||
func TestACL_Resources(t *testing.T) {
|
||||
t.Run("Resources", func(t *testing.T) {
|
||||
result := Resources.Resources()
|
||||
assert.Len(t, result, 21)
|
||||
assert.Len(t, result, 22)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ const (
|
|||
ResourcePassword Resource = "password"
|
||||
ResourceServices Resource = "services"
|
||||
ResourceUsers Resource = "users"
|
||||
ResourceSessions Resource = "sessions"
|
||||
ResourceLogs Resource = "logs"
|
||||
ResourceWebDAV Resource = "webdav"
|
||||
ResourceMetrics Resource = "metrics"
|
||||
|
|
|
@ -61,7 +61,12 @@ var Resources = ACL{
|
|||
RoleAdmin: GrantFullAccess,
|
||||
},
|
||||
ResourceUsers: Roles{
|
||||
RoleAdmin: Grant{AccessAll: true, AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
|
||||
RoleAdmin: Grant{AccessAll: true, AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
|
||||
RoleClient: Grant{AccessOwn: true, ActionView: true},
|
||||
},
|
||||
ResourceSessions: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
RoleDefault: Grant{AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
|
||||
},
|
||||
ResourceLogs: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
|
|
|
@ -18,16 +18,16 @@ func Auth(c *gin.Context, resource acl.Resource, grant acl.Permission) *entity.S
|
|||
// AuthAny checks if the user is authorized to access a resource with any of the specified permissions
|
||||
// and returns the session or nil otherwise.
|
||||
func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *entity.Session) {
|
||||
// Get the client IP and session ID from the request headers.
|
||||
ip := ClientIP(c)
|
||||
// Get client IP and auth token from the request headers.
|
||||
clientIp := ClientIP(c)
|
||||
authToken := AuthToken(c)
|
||||
|
||||
// Find active session to perform authorization check or deny if no session was found.
|
||||
if s = Session(authToken); s == nil {
|
||||
event.AuditWarn([]string{ip, "unauthenticated", "%s %s", "denied"}, grants.String(), string(resource))
|
||||
if s = Session(clientIp, authToken); s == nil {
|
||||
event.AuditWarn([]string{clientIp, "unauthenticated", "%s %s", "denied"}, grants.String(), string(resource))
|
||||
return entity.SessionStatusUnauthorized()
|
||||
} else {
|
||||
s.SetClientIP(ip)
|
||||
s.SetClientIP(clientIp)
|
||||
}
|
||||
|
||||
// If the request is from a client application, check its authorization based
|
||||
|
@ -35,31 +35,31 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
|
|||
if s.IsClient() {
|
||||
// Check ACL resource name against the permitted scope.
|
||||
if !s.HasScope(resource.String()) {
|
||||
event.AuditErr([]string{ip, "client %s", "session %s", "access %s", "denied"}, s.AuthID, s.RefID, string(resource))
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "access %s", "denied"}, s.AuthID, s.RefID, string(resource))
|
||||
return s
|
||||
}
|
||||
|
||||
// Perform an authorization check based on the ACL defaults for client applications.
|
||||
if acl.Resources.DenyAll(resource, acl.RoleClient, grants) {
|
||||
event.AuditErr([]string{ip, "client %s", "session %s", "%s %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Additionally check the user authorization if the client belongs to a user account.
|
||||
if s.NoUser() {
|
||||
// Allow access based on the ACL defaults for client applications.
|
||||
event.AuditInfo([]string{ip, "client %s", "session %s", "%s %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
} else if u := s.User(); !u.IsDisabled() && !u.IsUnknown() && u.IsRegistered() {
|
||||
if acl.Resources.DenyAll(resource, u.AclRole(), grants) {
|
||||
event.AuditErr([]string{ip, "client %s", "session %s", "%s %s as %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Allow access based on the user role.
|
||||
event.AuditInfo([]string{ip, "client %s", "session %s", "%s %s as %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
|
||||
} else {
|
||||
// Deny access if it is not a regular user account or the account has been disabled.
|
||||
event.AuditErr([]string{ip, "client %s", "session %s", "%s %s as unauthorized user", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as unauthorized user", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
|
@ -68,13 +68,13 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
|
|||
|
||||
// Otherwise, perform a regular ACL authorization check based on the user role.
|
||||
if u := s.User(); u.IsUnknown() || u.IsDisabled() {
|
||||
event.AuditWarn([]string{ip, "session %s", "%s %s as unauthorized user", "denied"}, s.RefID, grants.String(), string(resource))
|
||||
event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", "denied"}, s.RefID, grants.String(), string(resource))
|
||||
return entity.SessionStatusUnauthorized()
|
||||
} else if acl.Resources.DenyAll(resource, u.AclRole(), grants) {
|
||||
event.AuditErr([]string{ip, "session %s", "%s %s as %s", "denied"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
|
||||
event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", "denied"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
|
||||
return entity.SessionStatusForbidden()
|
||||
} else {
|
||||
event.AuditInfo([]string{ip, "session %s", "%s %s as %s", "granted"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
|
||||
event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", "granted"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@ func UpdateClientConfig() {
|
|||
// GET /api/v1/config
|
||||
func GetClientConfig(router *gin.RouterGroup) {
|
||||
router.GET("/config", func(c *gin.Context) {
|
||||
s := Session(AuthToken(c))
|
||||
sess := Session(ClientIP(c), AuthToken(c))
|
||||
conf := get.Config()
|
||||
|
||||
if s == nil {
|
||||
if sess == nil {
|
||||
c.JSON(http.StatusOK, conf.ClientPublic())
|
||||
} else {
|
||||
c.JSON(http.StatusOK, conf.ClientSession(s))
|
||||
c.JSON(http.StatusOK, conf.ClientSession(sess))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -31,9 +31,9 @@ func AddDownloadHeader(c *gin.Context, fileName string) {
|
|||
c.Header(header.ContentDisposition, fmt.Sprintf("attachment; filename=%s", fileName))
|
||||
}
|
||||
|
||||
// AddSessionHeader adds a session id header to the response.
|
||||
func AddSessionHeader(c *gin.Context, id string) {
|
||||
c.Header(header.XSessionID, id)
|
||||
// AddAuthTokenHeader adds an X-Auth-Token header to the response.
|
||||
func AddAuthTokenHeader(c *gin.Context, authToken string) {
|
||||
c.Header(header.XAuthToken, authToken)
|
||||
}
|
||||
|
||||
// AddContentTypeHeader adds a content type header to the response.
|
||||
|
|
|
@ -109,7 +109,7 @@ func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, pas
|
|||
Password: password,
|
||||
}))
|
||||
|
||||
authToken = r.Header().Get(header.XSessionID)
|
||||
authToken = r.Header().Get(header.XAuthToken)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,22 +3,33 @@ package api
|
|||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Session finds the client session for the specified
|
||||
// auth token, or returns nil if not found.
|
||||
func Session(authToken string) *entity.Session {
|
||||
// Session finds the client session for the specified auth token, or returns nil if not found.
|
||||
func Session(clientIp, authToken string) *entity.Session {
|
||||
// Skip authentication when running in public mode.
|
||||
if get.Config().Public() {
|
||||
return get.Session().Public()
|
||||
} else if !rnd.IsAuthAny(authToken) {
|
||||
}
|
||||
|
||||
// Fail if the auth token does not have a supported format.
|
||||
if !rnd.IsAuthAny(authToken) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the session based on the hashed auth
|
||||
// token used as id, or return nil otherwise.
|
||||
if s, err := get.Session().Get(rnd.SessionID(authToken)); err != nil {
|
||||
// Fail if authentication error rate limit is exceeded.
|
||||
if clientIp != "" && limiter.Auth.Reject(clientIp) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the session based on the hashed auth token, or return nil otherwise.
|
||||
if s, err := entity.FindSession(rnd.SessionID(authToken)); err != nil {
|
||||
if clientIp != "" {
|
||||
limiter.Auth.Reserve(clientIp)
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return s
|
||||
|
|
|
@ -11,17 +11,24 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// CreateSession creates a new client session and returns it as JSON if authentication was successful.
|
||||
//
|
||||
// POST /api/v1/session
|
||||
// POST /api/v1/sessions
|
||||
func CreateSession(router *gin.RouterGroup) {
|
||||
router.POST("/session", func(c *gin.Context) {
|
||||
createSessionHandler := func(c *gin.Context) {
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
var f form.Login
|
||||
|
||||
clientIp := ClientIP(c)
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "create session", "invalid request", "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "create session", "invalid request", "%s"}, err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
@ -40,8 +47,8 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
// Check limit for failed auth requests (max. 10 per minute).
|
||||
if limiter.Login.Reject(ClientIP(c)) {
|
||||
// Fail if authentication error rate limit is exceeded.
|
||||
if clientIp != "" && (limiter.Login.Reject(clientIp) || limiter.Auth.Reject(clientIp)) {
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
|
@ -50,7 +57,7 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
var isNew bool
|
||||
|
||||
// Find existing session, if any.
|
||||
if s := Session(AuthToken(c)); s != nil {
|
||||
if s := Session(clientIp, AuthToken(c)); s != nil {
|
||||
// Update existing session.
|
||||
sess = s
|
||||
} else {
|
||||
|
@ -64,25 +71,28 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "%s"}, err)
|
||||
event.AuditErr([]string{clientIp, "%s"}, err)
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if sess == nil {
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
|
||||
return
|
||||
} else if isNew {
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", "created"}, sess.RefID)
|
||||
event.AuditInfo([]string{clientIp, "session %s", "created"}, sess.RefID)
|
||||
} else {
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", "updated"}, sess.RefID)
|
||||
event.AuditInfo([]string{clientIp, "session %s", "updated"}, sess.RefID)
|
||||
}
|
||||
|
||||
// Add session id to response headers.
|
||||
AddSessionHeader(c, sess.AuthToken())
|
||||
// Add auth token to response header.
|
||||
AddAuthTokenHeader(c, sess.AuthToken())
|
||||
|
||||
// Response includes user data, session data, and client config values.
|
||||
response := CreateSessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess))
|
||||
|
||||
// Return JSON response.
|
||||
c.JSON(sess.HttpStatus(), response)
|
||||
})
|
||||
}
|
||||
|
||||
router.POST("/session", createSessionHandler)
|
||||
router.POST("/sessions", createSessionHandler)
|
||||
}
|
||||
|
|
|
@ -10,8 +10,10 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/internal/session"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
|
@ -19,8 +21,12 @@ import (
|
|||
//
|
||||
// DELETE /api/v1/session
|
||||
// DELETE /api/v1/session/:id
|
||||
// DELETE /api/v1/sessions/:id
|
||||
func DeleteSession(router *gin.RouterGroup) {
|
||||
deleteSessionHandler := func(c *gin.Context) {
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
// Abort if running in public mode.
|
||||
if get.Config().Public() {
|
||||
// Return JSON response for confirmation.
|
||||
|
@ -30,13 +36,23 @@ func DeleteSession(router *gin.RouterGroup) {
|
|||
|
||||
id := clean.ID(c.Param("id"))
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIP := ClientIP(c)
|
||||
// Get client IP and auth token from request headers.
|
||||
clientIp := ClientIP(c)
|
||||
authToken := AuthToken(c)
|
||||
|
||||
// Fail if authentication error rate limit is exceeded.
|
||||
if clientIp != "" && limiter.Auth.Reject(clientIp) {
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Find session based on auth token.
|
||||
sess, err := entity.FindSession(rnd.SessionID(AuthToken(c)))
|
||||
sess, err := entity.FindSession(rnd.SessionID(authToken))
|
||||
|
||||
if err != nil || sess == nil {
|
||||
if clientIp != "" {
|
||||
limiter.Auth.Reserve(clientIp)
|
||||
}
|
||||
Abort(c, http.StatusUnauthorized, i18n.ErrUnauthorized)
|
||||
return
|
||||
} else if sess.Abort(c) {
|
||||
|
@ -45,29 +61,29 @@ func DeleteSession(router *gin.RouterGroup) {
|
|||
|
||||
// Only admins may delete other sessions by ref id.
|
||||
if rnd.IsRefID(id) {
|
||||
if !acl.Resources.AllowAll(acl.ResourceUsers, sess.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
event.AuditErr([]string{clientIP, "session %s", "delete session %s as %s", "denied"}, sess.RefID, id, sess.User().AclRole())
|
||||
if !acl.Resources.AllowAll(acl.ResourceSessions, sess.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", "denied"}, sess.RefID, acl.ResourceSessions.String(), sess.User().AclRole())
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{clientIP, "session %s", "delete session %s as %s", "granted"}, sess.RefID, id, sess.User().AclRole())
|
||||
event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", "granted"}, sess.RefID, acl.ResourceSessions.String(), sess.User().AclRole())
|
||||
|
||||
if sess = entity.FindSessionByRefID(id); sess == nil {
|
||||
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
|
||||
return
|
||||
}
|
||||
} else if id != "" && sess.ID != id {
|
||||
event.AuditWarn([]string{clientIP, "session %s", "delete session as %s", "ids do not match"}, sess.RefID, sess.User().AclRole())
|
||||
event.AuditWarn([]string{clientIp, "session %s", "delete %s as %s", "ids do not match"}, sess.RefID, acl.ResourceSessions.String(), sess.User().AclRole())
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete session cache and database record.
|
||||
if err = sess.Delete(); err != nil {
|
||||
event.AuditErr([]string{clientIP, "session %s", "delete session as %s", "%s"}, sess.RefID, sess.User().AclRole(), err)
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete session as %s", "%s"}, sess.RefID, sess.User().AclRole(), err)
|
||||
} else {
|
||||
event.AuditDebug([]string{clientIP, "session %s", "deleted"}, sess.RefID)
|
||||
event.AuditDebug([]string{clientIp, "session %s", "deleted"}, sess.RefID)
|
||||
}
|
||||
|
||||
// Return JSON response for confirmation.
|
||||
|
@ -76,4 +92,5 @@ func DeleteSession(router *gin.RouterGroup) {
|
|||
|
||||
router.DELETE("/session", deleteSessionHandler)
|
||||
router.DELETE("/session/:id", deleteSessionHandler)
|
||||
router.DELETE("/sessions/:id", deleteSessionHandler)
|
||||
}
|
||||
|
|
|
@ -7,24 +7,34 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// GetSession returns the session data as JSON if authentication was successful.
|
||||
//
|
||||
// GET /api/v1/session
|
||||
// GET /api/v1/session/:id
|
||||
// GET /api/v1/sessions/:id
|
||||
func GetSession(router *gin.RouterGroup) {
|
||||
getSessionHandler := func(c *gin.Context) {
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
id := clean.ID(c.Param("id"))
|
||||
|
||||
// Check authentication token.
|
||||
if id == "" {
|
||||
// Abort if authentication token is missing or empty.
|
||||
// Abort if session id is provided but invalid.
|
||||
if id != "" && !rnd.IsSessionID(id) {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
conf := get.Config()
|
||||
|
||||
// Get client IP and auth token from request headers.
|
||||
clientIp := ClientIP(c)
|
||||
authToken := AuthToken(c)
|
||||
|
||||
// Skip authentication if app is running in public mode.
|
||||
|
@ -33,18 +43,25 @@ func GetSession(router *gin.RouterGroup) {
|
|||
sess = get.Session().Public()
|
||||
id = sess.ID
|
||||
authToken = sess.AuthToken()
|
||||
} else if clientIp != "" && limiter.Auth.Reject(clientIp) {
|
||||
// Fail if authentication error rate limit is exceeded.
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
} else {
|
||||
sess = Session(authToken)
|
||||
sess = Session(clientIp, authToken)
|
||||
}
|
||||
|
||||
switch {
|
||||
case sess == nil:
|
||||
if clientIp != "" {
|
||||
limiter.Auth.Reserve(clientIp)
|
||||
}
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
case sess.Expired(), sess.ID == "":
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
case sess.Invalid(), sess.ID != id && !conf.Public():
|
||||
case sess.Invalid(), id != "" && sess.ID != id && !conf.Public():
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
@ -52,8 +69,8 @@ func GetSession(router *gin.RouterGroup) {
|
|||
// Update user information.
|
||||
sess.RefreshUser()
|
||||
|
||||
// Add session id to response headers.
|
||||
AddSessionHeader(c, authToken)
|
||||
// Add auth token to response header.
|
||||
AddAuthTokenHeader(c, authToken)
|
||||
|
||||
// Response includes user data, session data, and client config values.
|
||||
response := GetSessionResponse(authToken, sess, get.Config().ClientSession(sess))
|
||||
|
@ -62,5 +79,7 @@ func GetSession(router *gin.RouterGroup) {
|
|||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
router.GET("/session", getSessionHandler)
|
||||
router.GET("/session/:id", getSessionHandler)
|
||||
router.GET("/sessions/:id", getSessionHandler)
|
||||
}
|
||||
|
|
|
@ -25,12 +25,15 @@ import (
|
|||
// POST /api/v1/oauth/token
|
||||
func CreateOAuthToken(router *gin.RouterGroup) {
|
||||
router.POST("/oauth/token", func(c *gin.Context) {
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIP := ClientIP(c)
|
||||
clientIp := ClientIP(c)
|
||||
|
||||
// Abort if running in public mode.
|
||||
if get.Config().Public() {
|
||||
event.AuditErr([]string{clientIP, "create client session", "disabled in public mode"})
|
||||
event.AuditErr([]string{clientIp, "create client session", "disabled in public mode"})
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
@ -45,20 +48,20 @@ func CreateOAuthToken(router *gin.RouterGroup) {
|
|||
f.ClientID = clientId
|
||||
f.ClientSecret = clientSecret
|
||||
} else if err = c.ShouldBind(&f); err != nil {
|
||||
event.AuditWarn([]string{clientIP, "create client session", "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "create client session", "%s"}, err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Check the credentials for completeness and the correct format.
|
||||
if err = f.Validate(); err != nil {
|
||||
event.AuditWarn([]string{clientIP, "create client session", "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "create client session", "%s"}, err)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
}
|
||||
|
||||
// Check limit for failed auth requests (max. 10 per minute).
|
||||
if limiter.Login.Reject(clientIP) {
|
||||
// Fail if authentication error rate limit is exceeded.
|
||||
if clientIp != "" && (limiter.Login.Reject(clientIp) || limiter.Auth.Reject(clientIp)) {
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
|
@ -68,22 +71,22 @@ func CreateOAuthToken(router *gin.RouterGroup) {
|
|||
|
||||
// Abort if the client ID or secret are invalid.
|
||||
if client == nil {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create session", "invalid client_id"}, f.ClientID)
|
||||
event.AuditWarn([]string{clientIp, "client %s", "create session", "invalid client id"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
limiter.Login.Reserve(clientIP)
|
||||
limiter.Login.Reserve(clientIp)
|
||||
return
|
||||
} else if !client.AuthEnabled {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create session", "authentication disabled"}, f.ClientID)
|
||||
event.AuditWarn([]string{clientIp, "client %s", "create session", "authentication disabled"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if method := client.Method(); !method.IsDefault() && method != authn.MethodOAuth2 {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create session", "method %s not supported"}, f.ClientID, clean.LogQuote(method.String()))
|
||||
event.AuditWarn([]string{clientIp, "client %s", "create session", "method %s not supported"}, f.ClientID, clean.LogQuote(method.String()))
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if client.WrongSecret(f.ClientSecret) {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create session", "invalid client_secret"}, f.ClientID)
|
||||
event.AuditWarn([]string{clientIp, "client %s", "create session", "invalid client secret"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
limiter.Login.Reserve(clientIP)
|
||||
limiter.Login.Reserve(clientIp)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -92,20 +95,20 @@ func CreateOAuthToken(router *gin.RouterGroup) {
|
|||
|
||||
// Try to log in and save session if successful.
|
||||
if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{clientIP, "client %s", "create session", "%s"}, f.ClientID, err)
|
||||
event.AuditErr([]string{clientIp, "client %s", "create session", "%s"}, f.ClientID, err)
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIP, "client %s", "create session", StatusFailed.String()}, f.ClientID)
|
||||
event.AuditErr([]string{clientIp, "client %s", "create session", StatusFailed.String()}, f.ClientID)
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
|
||||
return
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIP, "client %s", "session %s", "created"}, f.ClientID, sess.RefID)
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "created"}, f.ClientID, sess.RefID)
|
||||
}
|
||||
|
||||
// Deletes old client sessions above the configured limit.
|
||||
if deleted := client.EnforceAuthTokenLimit(); deleted > 0 {
|
||||
event.AuditInfo([]string{clientIP, "client %s", "%s deleted"}, f.ClientID, english.Plural(deleted, "old session", "old sessions"))
|
||||
event.AuditInfo([]string{clientIp, "client %s", "%s deleted"}, f.ClientID, english.Plural(deleted, "old session", "old sessions"))
|
||||
}
|
||||
|
||||
// Response includes access token, token type, and token lifetime.
|
||||
|
@ -125,12 +128,15 @@ func CreateOAuthToken(router *gin.RouterGroup) {
|
|||
// POST /api/v1/oauth/revoke
|
||||
func RevokeOAuthToken(router *gin.RouterGroup) {
|
||||
router.POST("/oauth/revoke", func(c *gin.Context) {
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIP := ClientIP(c)
|
||||
clientIp := ClientIP(c)
|
||||
|
||||
// Abort if running in public mode.
|
||||
if get.Config().Public() {
|
||||
event.AuditErr([]string{clientIP, "delete client session", "disabled in public mode"})
|
||||
event.AuditErr([]string{clientIp, "delete client session", "disabled in public mode"})
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
@ -144,7 +150,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
|
|||
|
||||
// Get the auth token to be revoked from the submitted form values or the request header.
|
||||
if err = c.ShouldBind(&f); err != nil && authToken == "" {
|
||||
event.AuditWarn([]string{clientIP, "delete client session", "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "delete client session", "%s"}, err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
} else if f.Empty() {
|
||||
|
@ -154,7 +160,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
|
|||
|
||||
// Check the token form values.
|
||||
if err = f.Validate(); err != nil {
|
||||
event.AuditWarn([]string{clientIP, "delete client session", "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "delete client session", "%s"}, err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
@ -163,28 +169,28 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
|
|||
sess, err := entity.FindSession(rnd.SessionID(f.AuthToken))
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err.Error())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized))
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized))
|
||||
return
|
||||
} else if sess.Abort(c) {
|
||||
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
return
|
||||
} else if !sess.IsClient() {
|
||||
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, i18n.NewResponse(http.StatusForbidden, i18n.ErrForbidden))
|
||||
return
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIP, "client %s", "session %s", "delete session as %s", "granted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "delete session as %s", "granted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
}
|
||||
|
||||
// Delete session cache and database record.
|
||||
if err = sess.Delete(); err != nil {
|
||||
// Log error.
|
||||
event.AuditErr([]string{clientIP, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err)
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err)
|
||||
|
||||
// Return JSON error.
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, i18n.NewResponse(http.StatusNotFound, i18n.ErrNotFound))
|
||||
|
@ -192,7 +198,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Log event.
|
||||
event.AuditInfo([]string{clientIP, "client %s", "session %s", "deleted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID))
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "deleted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID))
|
||||
|
||||
// Return JSON response for confirmation.
|
||||
c.JSON(http.StatusOK, DeleteSessionResponse(sess.ID))
|
||||
|
|
|
@ -17,8 +17,8 @@ import (
|
|||
func TestSession(t *testing.T) {
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
sess := get.Session().Public()
|
||||
assert.Equal(t, sess, Session(""))
|
||||
assert.Equal(t, sess, Session("638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
|
||||
assert.Equal(t, sess, Session("1.2.3.4", ""))
|
||||
assert.Equal(t, sess, Session("1.2.3.4", "1234ffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -213,13 +213,43 @@ func TestGetSession(t *testing.T) {
|
|||
GetSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
t.Logf("Session ID: %s", authToken)
|
||||
t.Logf("Auth Token: %s", authToken)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session", authToken)
|
||||
t.Logf("Response Body: %s", r.Body.String())
|
||||
id := gjson.Get(r.Body.String(), "session_id").String()
|
||||
assert.Equal(t, rnd.SessionID(authToken), id)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedRequestWithID", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
GetSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
t.Logf("Auth Token: %s", authToken)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
|
||||
t.Logf("Response Body: %s", r.Body.String())
|
||||
id := gjson.Get(r.Body.String(), "session_id").String()
|
||||
assert.Equal(t, rnd.SessionID(authToken), id)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedRequestSessionsWithID", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
GetSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
t.Logf("Auth Token: %s", authToken)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/sessions/"+rnd.SessionID(authToken), authToken)
|
||||
t.Logf("Response Body: %s", r.Body.String())
|
||||
id := gjson.Get(r.Body.String(), "session_id").String()
|
||||
assert.Equal(t, rnd.SessionID(authToken), id)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteSession(t *testing.T) {
|
||||
|
@ -231,11 +261,8 @@ func TestDeleteSession(t *testing.T) {
|
|||
DeleteSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
// f9ae12e95a01bcc7faae6497124cd721eaf13c1dad301dbc
|
||||
t.Logf("authToken: %s", authToken)
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), "")
|
||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedRequest", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
@ -245,9 +272,31 @@ func TestDeleteSession(t *testing.T) {
|
|||
DeleteSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session", authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedRequestWithID", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedRequestSessionsWithID", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/sessions/"+rnd.SessionID(authToken), authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedLogout", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
|
|
|
@ -35,7 +35,7 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
|
|||
if jsonErr := json.Unmarshal(m, &info); jsonErr != nil {
|
||||
// Do nothing.
|
||||
} else {
|
||||
if s := Session(info.AuthToken); s != nil {
|
||||
if s := Session(ws.RemoteAddr().String(), info.AuthToken); s != nil {
|
||||
wsAuth.mutex.Lock()
|
||||
wsAuth.sid[connId] = s.ID
|
||||
wsAuth.rid[connId] = s.RefID
|
||||
|
|
|
@ -154,6 +154,11 @@ func SessionStatusForbidden() *Session {
|
|||
return &Session{Status: http.StatusForbidden}
|
||||
}
|
||||
|
||||
// SessionStatusTooManyRequests returns a session with status too many requests (429).
|
||||
func SessionStatusTooManyRequests() *Session {
|
||||
return &Session{Status: http.StatusTooManyRequests}
|
||||
}
|
||||
|
||||
// FindSessionByRefID finds an existing session by ref ID.
|
||||
func FindSessionByRefID(refId string) *Session {
|
||||
if !rnd.IsRefID(refId) {
|
||||
|
@ -340,9 +345,15 @@ func (m *Session) AuthInfo() string {
|
|||
return fmt.Sprintf("%s (%s)", provider.Pretty(), method.Pretty())
|
||||
}
|
||||
|
||||
// Provider returns the authentication provider.
|
||||
func (m *Session) Provider() authn.ProviderType {
|
||||
return authn.Provider(m.AuthProvider)
|
||||
// SetAuthID sets a custom authentication identifier.
|
||||
func (m *Session) SetAuthID(id string) *Session {
|
||||
if id == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
m.AuthID = clean.Name(id)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Method returns the authentication method.
|
||||
|
@ -350,9 +361,20 @@ func (m *Session) Method() authn.MethodType {
|
|||
return authn.Method(m.AuthMethod)
|
||||
}
|
||||
|
||||
// IsClient checks whether this session is used to authenticate an API client.
|
||||
func (m *Session) IsClient() bool {
|
||||
return authn.Provider(m.AuthProvider).IsClient()
|
||||
// SetMethod sets a custom authentication method.
|
||||
func (m *Session) SetMethod(method authn.MethodType) *Session {
|
||||
if method == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
m.AuthMethod = method.String()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Provider returns the authentication provider.
|
||||
func (m *Session) Provider() authn.ProviderType {
|
||||
return authn.Provider(m.AuthProvider)
|
||||
}
|
||||
|
||||
// SetProvider updates the session's authentication provider.
|
||||
|
@ -366,6 +388,11 @@ func (m *Session) SetProvider(provider authn.ProviderType) *Session {
|
|||
return m
|
||||
}
|
||||
|
||||
// IsClient checks whether this session is used to authenticate an API client.
|
||||
func (m *Session) IsClient() bool {
|
||||
return authn.Provider(m.AuthProvider).IsClient()
|
||||
}
|
||||
|
||||
// ChangePassword changes the password of the current user.
|
||||
func (m *Session) ChangePassword(newPw string) (err error) {
|
||||
u := m.User()
|
||||
|
@ -465,8 +492,8 @@ func (m *Session) SetContext(c *gin.Context) *Session {
|
|||
}
|
||||
|
||||
// Set client ip address from request context.
|
||||
if ip := header.ClientIP(c); ip != "" {
|
||||
m.SetClientIP(ip)
|
||||
if clientIp := header.ClientIP(c); clientIp != "" {
|
||||
m.SetClientIP(clientIp)
|
||||
} else if m.ClientIP == "" {
|
||||
// Unit tests often do not set a client IP.
|
||||
m.SetClientIP(UnknownIP)
|
||||
|
@ -489,8 +516,8 @@ func (m *Session) UpdateContext(c *gin.Context) *Session {
|
|||
changed := false
|
||||
|
||||
// Set client ip address from request context.
|
||||
if ip := header.ClientIP(c); ip != "" && (ip != m.ClientIP || m.LoginIP == "") {
|
||||
m.SetClientIP(ip)
|
||||
if clientIp := header.ClientIP(c); clientIp != "" && (clientIp != m.ClientIP || m.LoginIP == "") {
|
||||
m.SetClientIP(clientIp)
|
||||
changed = true
|
||||
} else if m.ClientIP == "" {
|
||||
// Unit tests often do not set a client IP.
|
||||
|
@ -701,6 +728,8 @@ func (m *Session) Abort(c *gin.Context) bool {
|
|||
switch m.Status {
|
||||
case http.StatusUnauthorized:
|
||||
c.AbortWithStatusJSON(m.Status, i18n.NewResponse(m.Status, i18n.ErrUnauthorized))
|
||||
case http.StatusTooManyRequests:
|
||||
c.AbortWithStatusJSON(m.Status, gin.H{"error": "rate limit exceeded", "code": http.StatusTooManyRequests})
|
||||
default:
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, i18n.NewResponse(http.StatusForbidden, i18n.ErrForbidden))
|
||||
}
|
||||
|
|
|
@ -50,6 +50,21 @@ var SessionFixtures = SessionMap{
|
|||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
},
|
||||
"alice_token_personal": {
|
||||
authToken: "bSJu9-2sr54-ZOasm-8QusP",
|
||||
ID: rnd.SessionID("bSJu9-2sr54-ZOasm-8QusP"),
|
||||
RefID: "sess6ey1ykya",
|
||||
SessTimeout: -1,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
AuthScope: clean.Scope("*"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token_personal",
|
||||
LastActive: -1,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
},
|
||||
"alice_token_webdav": {
|
||||
authToken: "bHcZP-YxRbi-irKII-W1kpz",
|
||||
ID: rnd.SessionID("bHcZP-YxRbi-irKII-W1kpz"),
|
||||
|
|
|
@ -7,21 +7,28 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Auth checks if the credentials are valid and returns the user and authentication provider.
|
||||
var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider authn.ProviderType, err error) {
|
||||
name := f.Username()
|
||||
// Get username from login form.
|
||||
nameName := f.Username()
|
||||
|
||||
user = FindUserByName(name)
|
||||
err = AuthLocal(user, f, m)
|
||||
// Find registered user account.
|
||||
user = FindUserByName(nameName)
|
||||
|
||||
// Try local authentication.
|
||||
provider, err = AuthLocal(user, f, m, c)
|
||||
|
||||
if err != nil {
|
||||
return user, authn.ProviderNone, err
|
||||
|
@ -30,60 +37,116 @@ var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider
|
|||
// Update login timestamp.
|
||||
user.UpdateLoginTime()
|
||||
|
||||
return user, authn.ProviderLocal, err
|
||||
return user, provider, err
|
||||
}
|
||||
|
||||
// AuthSession returns the client session that belongs to the auth token provided, or returns nil if it was not found.
|
||||
func AuthSession(f form.Login, c *gin.Context) (sess *Session, user *User, err error) {
|
||||
if f.Password == "" {
|
||||
// Abort authentication if no token was provided.
|
||||
return nil, nil, fmt.Errorf("no auth secret provided")
|
||||
} else if !rnd.IsAuthSecret(f.Password) {
|
||||
// Abort authentication if token doesn't match expected format.
|
||||
return nil, nil, fmt.Errorf("auth secret does not match expected format")
|
||||
}
|
||||
|
||||
// Get session ID for the auth token provided.
|
||||
sid := rnd.SessionID(f.Password)
|
||||
|
||||
// Find the session based on the hashed token used as session ID and return it.
|
||||
sess, err = FindSession(sid)
|
||||
|
||||
// Log error and return nil if no matching session was found.
|
||||
if sess == nil || err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid auth secret")
|
||||
}
|
||||
|
||||
// Update the client IP and the user agent from
|
||||
// the request context if they have changed.
|
||||
sess.UpdateContext(c)
|
||||
|
||||
// Returns session and user if all checks have passed.
|
||||
return sess, sess.User(), nil
|
||||
}
|
||||
|
||||
// AuthLocal authenticates against the local user database with the specified username and password.
|
||||
func AuthLocal(user *User, f form.Login, m *Session) (err error) {
|
||||
name := f.Username()
|
||||
func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (authn.ProviderType, error) {
|
||||
// Get client IP from request context.
|
||||
clientIp := header.ClientIP(c)
|
||||
|
||||
// User found?
|
||||
// Get username from login form.
|
||||
userName := f.Username()
|
||||
|
||||
// Check if a session has been created.
|
||||
if m == nil {
|
||||
event.AuditErr([]string{clientIp, "login as %s", "invalid session"}, clean.LogQuote(userName))
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Check if user account exists.
|
||||
if user == nil {
|
||||
message := "account not found"
|
||||
if m != nil {
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
}
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Login allowed?
|
||||
if !user.Provider().IsDefault() && !user.Provider().IsLocal() {
|
||||
message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String())
|
||||
if m != nil {
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
}
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else if !user.CanLogIn() {
|
||||
message := "account disabled"
|
||||
if m != nil {
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
}
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Password valid?
|
||||
// Authentication with personal access token if a valid secret has been provided as password.
|
||||
if authSess, authUser, err := AuthSession(f, c); err == nil {
|
||||
if !authUser.IsRegistered() || authUser.UserUID != user.UserUID {
|
||||
message := "incorrect user"
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s with auth secret", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else if !authSess.IsClient() || authSess.Method() != authn.MethodAccessToken || !authSess.HasScope(acl.ResourceSessions.String()) {
|
||||
message := "unauthorized"
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s with auth secret", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else {
|
||||
m.SetAuthID(authSess.AuthID)
|
||||
m.SetMethod(authn.MethodSession)
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s with auth secret", "succeeded"}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginInfo(clientIp, "api", userName, m.UserAgent)
|
||||
return authn.ProviderClient, err
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, check account password.
|
||||
if user.WrongPassword(f.Password) {
|
||||
message := "incorrect password"
|
||||
if m != nil {
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditErr([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
}
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else if m != nil {
|
||||
event.AuditInfo([]string{m.IP(), "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginInfo(m.IP(), "api", name, m.UserAgent)
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginInfo(clientIp, "api", userName, m.UserAgent)
|
||||
}
|
||||
|
||||
return err
|
||||
return authn.ProviderLocal, nil
|
||||
}
|
||||
|
||||
// LogIn performs authentication checks against the specified login form.
|
||||
|
|
|
@ -5,78 +5,246 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestAuthSession(t *testing.T) {
|
||||
t.Run("RandomAuthSecret", func(t *testing.T) {
|
||||
// Create test request form.
|
||||
f := form.Login{
|
||||
UserName: "alice",
|
||||
Password: rnd.AuthSecret(),
|
||||
}
|
||||
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
authSess, authUser, authErr := AuthSession(f, c)
|
||||
|
||||
assert.Nil(t, authSess)
|
||||
assert.Nil(t, authUser)
|
||||
assert.Error(t, authErr)
|
||||
})
|
||||
t.Run("RandomAuthToken", func(t *testing.T) {
|
||||
// Create test request form.
|
||||
f := form.Login{
|
||||
UserName: "alice",
|
||||
Password: rnd.AuthToken(),
|
||||
}
|
||||
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
authSess, authUser, authErr := AuthSession(f, c)
|
||||
|
||||
assert.Nil(t, authSess)
|
||||
assert.Nil(t, authUser)
|
||||
assert.Error(t, authErr)
|
||||
})
|
||||
t.Run("AliceAuthToken", func(t *testing.T) {
|
||||
s := SessionFixtures.Get("alice_token")
|
||||
|
||||
// Create test request form.
|
||||
f := form.Login{
|
||||
UserName: "alice",
|
||||
Password: s.AuthToken(),
|
||||
}
|
||||
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
authSess, authUser, authErr := AuthSession(f, c)
|
||||
|
||||
assert.Nil(t, authSess)
|
||||
assert.Nil(t, authUser)
|
||||
assert.Error(t, authErr)
|
||||
})
|
||||
t.Run("AliceTokenPersonal", func(t *testing.T) {
|
||||
s := SessionFixtures.Get("alice_token_personal")
|
||||
u := FindUserByName("alice")
|
||||
|
||||
// Create test request form.
|
||||
f := form.Login{
|
||||
UserName: "alice",
|
||||
Password: s.AuthToken(),
|
||||
}
|
||||
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
authSess, authUser, authErr := AuthSession(f, c)
|
||||
|
||||
if authErr != nil {
|
||||
t.Fatal(authErr)
|
||||
}
|
||||
|
||||
assert.NotNil(t, authSess)
|
||||
assert.NotNil(t, authUser)
|
||||
|
||||
assert.Equal(t, u.UserUID, s.UserUID)
|
||||
assert.Equal(t, u.Username(), s.Username())
|
||||
assert.Equal(t, authUser.UserUID, authSess.UserUID)
|
||||
assert.Equal(t, authUser.Username(), authSess.Username())
|
||||
assert.Equal(t, authUser.UserUID, authUser.UserUID)
|
||||
assert.Equal(t, authUser.Username(), authUser.Username())
|
||||
|
||||
assert.True(t, authSess.IsRegistered())
|
||||
assert.True(t, authSess.HasUser())
|
||||
|
||||
assert.True(t, authSess.HasScope(acl.ResourceWebDAV.String()))
|
||||
assert.True(t, authSess.HasScope(acl.ResourceSessions.String()))
|
||||
})
|
||||
t.Run("AliceTokenWebdav", func(t *testing.T) {
|
||||
s := SessionFixtures.Get("alice_token_webdav")
|
||||
u := FindUserByName("alice")
|
||||
|
||||
// Create test request form.
|
||||
f := form.Login{
|
||||
UserName: "alice",
|
||||
Password: s.AuthToken(),
|
||||
}
|
||||
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(f))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
authSess, authUser, authErr := AuthSession(f, c)
|
||||
|
||||
if authErr != nil {
|
||||
t.Fatal(authErr)
|
||||
}
|
||||
|
||||
assert.NotNil(t, authSess)
|
||||
assert.NotNil(t, authUser)
|
||||
|
||||
assert.Equal(t, u.UserUID, s.UserUID)
|
||||
assert.Equal(t, u.Username(), s.Username())
|
||||
assert.Equal(t, authUser.UserUID, authSess.UserUID)
|
||||
assert.Equal(t, authUser.Username(), authSess.Username())
|
||||
assert.Equal(t, authUser.UserUID, authUser.UserUID)
|
||||
assert.Equal(t, authUser.Username(), authUser.Username())
|
||||
|
||||
assert.True(t, authSess.IsRegistered())
|
||||
assert.True(t, authSess.HasUser())
|
||||
|
||||
assert.True(t, authSess.HasScope(acl.ResourceWebDAV.String()))
|
||||
assert.False(t, authSess.HasScope(acl.ResourceSessions.String()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthLocal(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
|
||||
m := FindSessionByRefID("sessxkkcabch")
|
||||
|
||||
u := FindUserByName("alice")
|
||||
|
||||
// Create test request form.
|
||||
frm := form.Login{
|
||||
UserName: "alice",
|
||||
Password: "Alice123!",
|
||||
}
|
||||
|
||||
if err := AuthLocal(u, frm, m); err != nil {
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
if provider, err := AuthLocal(u, frm, m, c); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, authn.ProviderLocal, provider)
|
||||
}
|
||||
})
|
||||
t.Run("Wrong credentials", func(t *testing.T) {
|
||||
|
||||
m := FindSessionByRefID("sessxkkcabch")
|
||||
|
||||
u := FindUserByName("alice")
|
||||
|
||||
// Create test request form.
|
||||
frm := form.Login{
|
||||
UserName: "alice",
|
||||
Password: "photoprism",
|
||||
}
|
||||
|
||||
if err := AuthLocal(u, frm, m); err == nil {
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
if provider, err := AuthLocal(u, frm, m, c); err == nil {
|
||||
t.Fatal("auth should fail")
|
||||
} else {
|
||||
assert.Equal(t, authn.ProviderNone, provider)
|
||||
}
|
||||
})
|
||||
t.Run("No login rights", func(t *testing.T) {
|
||||
|
||||
m := &Session{}
|
||||
|
||||
u := FindUserByName("friend")
|
||||
|
||||
u.CanLogin = false
|
||||
|
||||
// Create test request form.
|
||||
frm := form.Login{
|
||||
UserName: "friend",
|
||||
Password: "!Friend321",
|
||||
}
|
||||
|
||||
if err := AuthLocal(u, frm, m); err == nil {
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
if provider, err := AuthLocal(u, frm, m, c); err == nil {
|
||||
t.Fatal("auth should fail")
|
||||
} else {
|
||||
assert.Equal(t, authn.ProviderNone, provider)
|
||||
}
|
||||
|
||||
u.CanLogin = true
|
||||
})
|
||||
t.Run("Authentication disabled", func(t *testing.T) {
|
||||
|
||||
m := &Session{}
|
||||
|
||||
u := FindUserByName("friend")
|
||||
|
||||
u.SetProvider(authn.ProviderNone)
|
||||
|
||||
// Create test request form.
|
||||
frm := form.Login{
|
||||
UserName: "friend",
|
||||
Password: "!Friend321",
|
||||
}
|
||||
|
||||
if err := AuthLocal(u, frm, m); err == nil {
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Check authentication result.
|
||||
if provider, err := AuthLocal(u, frm, m, c); err == nil {
|
||||
t.Fatal("auth should fail")
|
||||
} else {
|
||||
assert.Equal(t, authn.ProviderNone, provider)
|
||||
}
|
||||
|
||||
u.SetProvider(authn.ProviderLocal)
|
||||
|
@ -85,9 +253,7 @@ func TestAuthLocal(t *testing.T) {
|
|||
|
||||
func TestSessionLogIn(t *testing.T) {
|
||||
const clientIp = "1.2.3.4"
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(rec)
|
||||
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
|
@ -99,12 +265,13 @@ func TestSessionLogIn(t *testing.T) {
|
|||
Password: "photoprism",
|
||||
}
|
||||
|
||||
// Create HTTP request.
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
ctx.Request.RemoteAddr = "1.2.3.4"
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Try to log in.
|
||||
if err := m.LogIn(frm, ctx); err != nil {
|
||||
if err := m.LogIn(frm, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
@ -118,12 +285,13 @@ func TestSessionLogIn(t *testing.T) {
|
|||
Password: "wrong",
|
||||
}
|
||||
|
||||
// Create HTTP request.
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
ctx.Request.RemoteAddr = "1.2.3.4"
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Try to log in.
|
||||
if err := m.LogIn(frm, ctx); err == nil {
|
||||
if err := m.LogIn(frm, c); err == nil {
|
||||
t.Fatal("login should fail")
|
||||
}
|
||||
})
|
||||
|
@ -137,12 +305,13 @@ func TestSessionLogIn(t *testing.T) {
|
|||
Password: "password",
|
||||
}
|
||||
|
||||
// Create HTTP request.
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
ctx.Request.RemoteAddr = "1.2.3.4"
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Try to log in.
|
||||
if err := m.LogIn(frm, ctx); err == nil {
|
||||
if err := m.LogIn(frm, c); err == nil {
|
||||
t.Fatal("login should fail")
|
||||
}
|
||||
})
|
||||
|
@ -155,12 +324,13 @@ func TestSessionLogIn(t *testing.T) {
|
|||
ShareToken: "1jxf3jfn2k",
|
||||
}
|
||||
|
||||
// Create HTTP request.
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
ctx.Request.RemoteAddr = "1.2.3.4"
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Try to log in.
|
||||
if err := m.LogIn(frm, ctx); err != nil {
|
||||
if err := m.LogIn(frm, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
@ -174,12 +344,13 @@ func TestSessionLogIn(t *testing.T) {
|
|||
ShareToken: "1jxf3jfxxx",
|
||||
}
|
||||
|
||||
// Create HTTP request.
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
ctx.Request.RemoteAddr = "1.2.3.4"
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Try to log in.
|
||||
if err := m.LogIn(frm, ctx); err == nil {
|
||||
if err := m.LogIn(frm, c); err == nil {
|
||||
t.Fatal("login should fail")
|
||||
}
|
||||
})
|
||||
|
@ -193,12 +364,13 @@ func TestSessionLogIn(t *testing.T) {
|
|||
ShareToken: "1jxf3jfn2k",
|
||||
}
|
||||
|
||||
// Create HTTP request.
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
ctx.Request.RemoteAddr = "1.2.3.4"
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Try to log in.
|
||||
if err := m.LogIn(frm, ctx); err != nil {
|
||||
if err := m.LogIn(frm, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
@ -212,12 +384,13 @@ func TestSessionLogIn(t *testing.T) {
|
|||
ShareToken: "1jxf3jfxxx",
|
||||
}
|
||||
|
||||
// Create HTTP request.
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
ctx.Request.RemoteAddr = "1.2.3.4"
|
||||
// Create test request context.
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/session", form.AsReader(frm))
|
||||
c.Request.RemoteAddr = "1.2.3.4"
|
||||
|
||||
// Try to log in.
|
||||
if err := m.LogIn(frm, ctx); err == nil {
|
||||
if err := m.LogIn(frm, c); err == nil {
|
||||
t.Fatal("login should fail")
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -130,13 +131,19 @@ func TestDeleteClientSessions(t *testing.T) {
|
|||
|
||||
func TestSessionStatusUnauthorized(t *testing.T) {
|
||||
m := SessionStatusUnauthorized()
|
||||
assert.Equal(t, 401, m.Status)
|
||||
assert.Equal(t, http.StatusUnauthorized, m.Status)
|
||||
assert.IsType(t, &Session{}, m)
|
||||
}
|
||||
|
||||
func TestSessionStatusForbidden(t *testing.T) {
|
||||
m := SessionStatusForbidden()
|
||||
assert.Equal(t, 403, m.Status)
|
||||
assert.Equal(t, http.StatusForbidden, m.Status)
|
||||
assert.IsType(t, &Session{}, m)
|
||||
}
|
||||
|
||||
func TestSessionStatusTooManyRequests(t *testing.T) {
|
||||
m := SessionStatusTooManyRequests()
|
||||
assert.Equal(t, http.StatusTooManyRequests, m.Status)
|
||||
assert.IsType(t, &Session{}, m)
|
||||
}
|
||||
|
||||
|
|
20
internal/server/limiter/auth.go
Normal file
20
internal/server/limiter/auth.go
Normal 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)
|
|
@ -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)
|
|
@ -7,9 +7,9 @@ import (
|
|||
)
|
||||
|
||||
// Middleware registers the IP rate limiter middleware.
|
||||
func Middleware(ip *Limit) gin.HandlerFunc {
|
||||
func Middleware(limiter *Limit) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if l := ip.IP(c.ClientIP()); !l.Allow() {
|
||||
if l := limiter.IP(c.ClientIP()); !l.Allow() {
|
||||
c.AbortWithStatus(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type MethodType string
|
|||
// Authentication methods.
|
||||
const (
|
||||
MethodDefault MethodType = "default"
|
||||
MethodSession MethodType = "session"
|
||||
MethodAccessToken MethodType = "access_token"
|
||||
MethodOAuth2 MethodType = "oauth2"
|
||||
MethodOIDC MethodType = "oidc"
|
||||
|
|
|
@ -2,6 +2,6 @@ package header
|
|||
|
||||
const (
|
||||
CacheControl = "Cache-Control"
|
||||
CacheControlNoCache = "no-cache"
|
||||
CacheControlNoStore = "no-store"
|
||||
CacheControlNoCache = "no-cache"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue