pgkiln 0.62.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +4 -0
- package/db/migrations/131_builder_sso.sql +69 -0
- package/db/migrations/132_app_user_mfa.sql +58 -0
- package/docs/apex-feature-parity.md +5 -2
- package/docs/guide/03-builder.md +46 -0
- package/docs/guide/08-security.md +48 -6
- package/docs/guide/09-reference.md +5 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +9 -0
- package/docs/guide/18-cli.md +1 -0
- package/package.json +1 -1
- package/public/app.css +10 -0
- package/public/builder.css +4 -0
- package/src/account-mfa.ts +93 -0
- package/src/builder/advisor.ts +8 -0
- package/src/builder/builder-sso.ts +135 -0
- package/src/builder/i18n/de.ts +56 -0
- package/src/builder/i18n/en.ts +56 -0
- package/src/builder/i18n/es.ts +56 -0
- package/src/builder/i18n/fr.ts +56 -0
- package/src/builder/i18n/nl.ts +56 -0
- package/src/builder/i18n/pt.ts +56 -0
- package/src/builder/ldap.ts +2 -0
- package/src/builder/login-page.ts +26 -2
- package/src/builder/routes.ts +137 -21
- package/src/builder/security.ts +3 -1
- package/src/builder/ui.ts +2 -1
- package/src/builder/users.ts +36 -3
- package/src/builder/workspaces.ts +37 -3
- package/src/cli/main.ts +19 -5
- package/src/devsso.ts +149 -0
- package/src/i18n/ar.ts +34 -0
- package/src/i18n/cs.ts +34 -0
- package/src/i18n/da.ts +34 -0
- package/src/i18n/de.ts +34 -0
- package/src/i18n/el.ts +34 -0
- package/src/i18n/es.ts +34 -0
- package/src/i18n/fi.ts +34 -0
- package/src/i18n/fr.ts +34 -0
- package/src/i18n/he.ts +34 -0
- package/src/i18n/it.ts +34 -0
- package/src/i18n/ja.ts +34 -0
- package/src/i18n/ko.ts +34 -0
- package/src/i18n/nb.ts +34 -0
- package/src/i18n/pl.ts +34 -0
- package/src/i18n/pt.ts +34 -0
- package/src/i18n/ru.ts +34 -0
- package/src/i18n/sv.ts +34 -0
- package/src/i18n/tr.ts +34 -0
- package/src/i18n/uk.ts +34 -0
- package/src/i18n/zh.ts +34 -0
- package/src/i18n.ts +68 -0
- package/src/metadata.ts +4 -1
- package/src/mfa.ts +76 -52
- package/src/remember.ts +1 -1
- package/src/runtime/account.ts +4 -1
- package/src/runtime/routes.ts +24 -9
- package/src/runtime/two-step.ts +395 -0
- package/src/saml.ts +13 -7
- package/src/sharedlogin.ts +1 -1
- package/src/sso.ts +37 -7
package/src/i18n/ko.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const ko: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "그룹별 {what}",
|
|
806
806
|
'ai_filter.needs_number': "{fn}에는 숫자 열이 필요합니다.",
|
|
807
807
|
'ai_filter.invalid': "이 변경은 할 수 없습니다: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "2단계 로그인",
|
|
810
|
+
"mfa.step_intro": "인증 앱의 6자리 코드 또는 복구 코드 중 하나를 입력하세요.",
|
|
811
|
+
"mfa.code": "코드",
|
|
812
|
+
"mfa.verify": "확인",
|
|
813
|
+
"mfa.wrong": "코드가 올바르지 않거나 이미 사용되었습니다.",
|
|
814
|
+
"mfa.expired": "로그인 시간이 너무 오래 걸렸습니다. 다시 로그인하세요.",
|
|
815
|
+
"mfa.enrol_title": "2단계 로그인 설정",
|
|
816
|
+
"mfa.enrol_intro": "이 애플리케이션은 로그인할 때 인증 앱의 코드를 요구합니다. 계속하려면 지금 추가하세요.",
|
|
817
|
+
"mfa.scan": "Google Authenticator, Microsoft Authenticator, 1Password 같은 인증 앱으로 QR 코드를 스캔하세요.",
|
|
818
|
+
"mfa.or_enter": "또는 다음 키를 입력하세요:",
|
|
819
|
+
"mfa.enter_code": "앱에 표시된 6자리 코드를 입력하세요.",
|
|
820
|
+
"mfa.qr_label": "인증 앱용 QR 코드",
|
|
821
|
+
"mfa.codes_title": "복구 코드",
|
|
822
|
+
"mfa.codes_once": "이 코드를 안전한 곳에 보관하세요. 휴대폰이 없을 때 코드마다 한 번씩 로그인할 수 있습니다. 지금만 표시됩니다.",
|
|
823
|
+
"mfa.codes_saved": "이 코드를 저장했습니다",
|
|
824
|
+
"mfa.continue": "계속",
|
|
825
|
+
"mfa.no_key": "이 서버에는 아직 2단계 로그인이 설정되지 않았습니다. 관리자에게 문의하세요.",
|
|
826
|
+
"mfa.app_name": "이름",
|
|
827
|
+
"mfa.app_default": "인증 앱",
|
|
828
|
+
"mfa.on": "로그인할 때 인증 앱의 코드도 입력합니다.",
|
|
829
|
+
"mfa.off": "인증 앱을 추가하면 로그인할 때 두 번째 단계로 계정을 보호할 수 있습니다.",
|
|
830
|
+
"mfa.required_you": "이 애플리케이션은 2단계 로그인을 요구합니다. 인증 앱을 추가하세요.",
|
|
831
|
+
"mfa.add_app": "인증 앱 추가",
|
|
832
|
+
"mfa.confirm_app": "앱 추가",
|
|
833
|
+
"mfa.cancel": "취소",
|
|
834
|
+
"mfa.remove": "제거",
|
|
835
|
+
"mfa.remove_code": "현재 코드 또는 복구 코드",
|
|
836
|
+
"mfa.removed": "인증 앱을 제거했습니다.",
|
|
837
|
+
"mfa.keep_one": "이 애플리케이션은 2단계 로그인을 요구합니다. 이 앱을 제거하기 전에 다른 앱을 추가하세요.",
|
|
838
|
+
"mfa.added": "인증 앱을 추가했습니다.",
|
|
839
|
+
"mfa.codes_left": "남은 복구 코드: {n}",
|
|
840
|
+
"mfa.codes_new": "새 복구 코드",
|
|
841
|
+
"mfa.recovery_used": "복구 코드로 로그인했습니다. 남은 복구 코드: {n}.",
|
|
808
842
|
};
|
package/src/i18n/nb.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const nb: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "{what} per gruppe",
|
|
806
806
|
'ai_filter.needs_number': "{fn} krever en kolonne med tall.",
|
|
807
807
|
'ai_filter.invalid': "Den endringen er ikke mulig: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "Pålogging i to trinn",
|
|
810
|
+
"mfa.step_intro": "Skriv inn den 6-sifrede koden fra autentiseringsappen, eller en av gjenopprettingskodene dine.",
|
|
811
|
+
"mfa.code": "Kode",
|
|
812
|
+
"mfa.verify": "Bekreft",
|
|
813
|
+
"mfa.wrong": "Koden er feil, eller den er allerede brukt.",
|
|
814
|
+
"mfa.expired": "Påloggingen tok for lang tid. Logg på på nytt.",
|
|
815
|
+
"mfa.enrol_title": "Konfigurer pålogging i to trinn",
|
|
816
|
+
"mfa.enrol_intro": "Denne applikasjonen ber om en kode fra en autentiseringsapp når du logger på. Legg til en nå for å fortsette.",
|
|
817
|
+
"mfa.scan": "Skann QR-koden med en autentiseringsapp, for eksempel Google Authenticator, Microsoft Authenticator eller 1Password.",
|
|
818
|
+
"mfa.or_enter": "Eller skriv inn denne nøkkelen:",
|
|
819
|
+
"mfa.enter_code": "Skriv inn den 6-sifrede koden appen viser.",
|
|
820
|
+
"mfa.qr_label": "QR-kode for en autentiseringsapp",
|
|
821
|
+
"mfa.codes_title": "Gjenopprettingskoder",
|
|
822
|
+
"mfa.codes_once": "Oppbevar kodene på et trygt sted. Hver kode logger deg på én gang når du ikke har telefonen. De vises bare nå.",
|
|
823
|
+
"mfa.codes_saved": "Jeg har lagret kodene",
|
|
824
|
+
"mfa.continue": "Fortsett",
|
|
825
|
+
"mfa.no_key": "Pålogging i to trinn er ikke konfigurert på denne serveren ennå. Kontakt administratoren.",
|
|
826
|
+
"mfa.app_name": "Navn",
|
|
827
|
+
"mfa.app_default": "Autentiseringsapp",
|
|
828
|
+
"mfa.on": "Når du logger på, oppgir du også en kode fra autentiseringsappen.",
|
|
829
|
+
"mfa.off": "Legg til en autentiseringsapp for å beskytte kontoen med et ekstra trinn når du logger på.",
|
|
830
|
+
"mfa.required_you": "Denne applikasjonen krever pålogging i to trinn for deg: legg til en autentiseringsapp.",
|
|
831
|
+
"mfa.add_app": "Legg til en autentiseringsapp",
|
|
832
|
+
"mfa.confirm_app": "Legg til app",
|
|
833
|
+
"mfa.cancel": "Avbryt",
|
|
834
|
+
"mfa.remove": "Fjern",
|
|
835
|
+
"mfa.remove_code": "En gjeldende kode eller en gjenopprettingskode",
|
|
836
|
+
"mfa.removed": "Autentiseringsappen ble fjernet.",
|
|
837
|
+
"mfa.keep_one": "Denne applikasjonen krever pålogging i to trinn for deg: legg til en annen app før du fjerner denne.",
|
|
838
|
+
"mfa.added": "Autentiseringsappen ble lagt til.",
|
|
839
|
+
"mfa.codes_left": "Gjenværende gjenopprettingskoder: {n}",
|
|
840
|
+
"mfa.codes_new": "Nye gjenopprettingskoder",
|
|
841
|
+
"mfa.recovery_used": "Du logget på med en gjenopprettingskode. Gjenværende gjenopprettingskoder: {n}.",
|
|
808
842
|
};
|
package/src/i18n/pl.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const pl: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "{what} na grupę",
|
|
806
806
|
'ai_filter.needs_number': "{fn} wymaga kolumny z liczbami.",
|
|
807
807
|
'ai_filter.invalid': "Ta zmiana nie jest możliwa: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "Logowanie dwuetapowe",
|
|
810
|
+
"mfa.step_intro": "Wpisz 6-cyfrowy kod z aplikacji uwierzytelniającej albo jeden z kodów odzyskiwania.",
|
|
811
|
+
"mfa.code": "Kod",
|
|
812
|
+
"mfa.verify": "Zweryfikuj",
|
|
813
|
+
"mfa.wrong": "Kod jest nieprawidłowy albo został już użyty.",
|
|
814
|
+
"mfa.expired": "Logowanie trwało zbyt długo. Zaloguj się ponownie.",
|
|
815
|
+
"mfa.enrol_title": "Skonfiguruj logowanie dwuetapowe",
|
|
816
|
+
"mfa.enrol_intro": "Ta aplikacja wymaga przy logowaniu kodu z aplikacji uwierzytelniającej. Dodaj ją teraz, aby kontynuować.",
|
|
817
|
+
"mfa.scan": "Zeskanuj kod QR aplikacją uwierzytelniającą, np. Google Authenticator, Microsoft Authenticator lub 1Password.",
|
|
818
|
+
"mfa.or_enter": "Albo wpisz ten klucz:",
|
|
819
|
+
"mfa.enter_code": "Wpisz 6-cyfrowy kod wyświetlany w aplikacji.",
|
|
820
|
+
"mfa.qr_label": "Kod QR dla aplikacji uwierzytelniającej",
|
|
821
|
+
"mfa.codes_title": "Kody odzyskiwania",
|
|
822
|
+
"mfa.codes_once": "Przechowuj te kody w bezpiecznym miejscu. Każdy z nich pozwala zalogować się raz, gdy nie masz telefonu. Są wyświetlane tylko teraz.",
|
|
823
|
+
"mfa.codes_saved": "Zapisałem te kody",
|
|
824
|
+
"mfa.continue": "Dalej",
|
|
825
|
+
"mfa.no_key": "Logowanie dwuetapowe nie jest jeszcze skonfigurowane na tym serwerze. Skontaktuj się z administratorem.",
|
|
826
|
+
"mfa.app_name": "Nazwa",
|
|
827
|
+
"mfa.app_default": "Aplikacja uwierzytelniająca",
|
|
828
|
+
"mfa.on": "Przy logowaniu podajesz też kod z aplikacji uwierzytelniającej.",
|
|
829
|
+
"mfa.off": "Dodaj aplikację uwierzytelniającą, aby chronić konto drugim krokiem przy logowaniu.",
|
|
830
|
+
"mfa.required_you": "Ta aplikacja wymaga od Ciebie logowania dwuetapowego: dodaj aplikację uwierzytelniającą.",
|
|
831
|
+
"mfa.add_app": "Dodaj aplikację uwierzytelniającą",
|
|
832
|
+
"mfa.confirm_app": "Dodaj aplikację",
|
|
833
|
+
"mfa.cancel": "Anuluj",
|
|
834
|
+
"mfa.remove": "Usuń",
|
|
835
|
+
"mfa.remove_code": "Bieżący kod lub kod odzyskiwania",
|
|
836
|
+
"mfa.removed": "Aplikacja uwierzytelniająca została usunięta.",
|
|
837
|
+
"mfa.keep_one": "Ta aplikacja wymaga od Ciebie logowania dwuetapowego: dodaj inną aplikację, zanim usuniesz tę.",
|
|
838
|
+
"mfa.added": "Aplikacja uwierzytelniająca została dodana.",
|
|
839
|
+
"mfa.codes_left": "Pozostałe kody odzyskiwania: {n}",
|
|
840
|
+
"mfa.codes_new": "Nowe kody odzyskiwania",
|
|
841
|
+
"mfa.recovery_used": "Zalogowano kodem odzyskiwania. Pozostałe kody odzyskiwania: {n}.",
|
|
808
842
|
};
|
package/src/i18n/pt.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const pt: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "{what} por grupo",
|
|
806
806
|
'ai_filter.needs_number': "{fn} precisa de uma coluna com números.",
|
|
807
807
|
'ai_filter.invalid': "Essa alteração não é possível: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "Início de sessão em dois passos",
|
|
810
|
+
"mfa.step_intro": "Introduza o código de 6 dígitos da sua aplicação de autenticação, ou um dos seus códigos de recuperação.",
|
|
811
|
+
"mfa.code": "Código",
|
|
812
|
+
"mfa.verify": "Verificar",
|
|
813
|
+
"mfa.wrong": "Esse código não está correto ou já foi utilizado.",
|
|
814
|
+
"mfa.expired": "O início de sessão demorou demasiado. Inicie sessão novamente.",
|
|
815
|
+
"mfa.enrol_title": "Configurar o início de sessão em dois passos",
|
|
816
|
+
"mfa.enrol_intro": "Esta aplicação pede um código de uma aplicação de autenticação ao iniciar sessão. Adicione uma agora para continuar.",
|
|
817
|
+
"mfa.scan": "Leia o código QR com uma aplicação de autenticação, como Google Authenticator, Microsoft Authenticator ou 1Password.",
|
|
818
|
+
"mfa.or_enter": "Ou introduza esta chave:",
|
|
819
|
+
"mfa.enter_code": "Introduza o código de 6 dígitos que a aplicação mostra.",
|
|
820
|
+
"mfa.qr_label": "Código QR para uma aplicação de autenticação",
|
|
821
|
+
"mfa.codes_title": "Códigos de recuperação",
|
|
822
|
+
"mfa.codes_once": "Guarde estes códigos num local seguro. Cada um permite iniciar sessão uma vez quando não tem o telemóvel. Só são mostrados agora.",
|
|
823
|
+
"mfa.codes_saved": "Guardei estes códigos",
|
|
824
|
+
"mfa.continue": "Continuar",
|
|
825
|
+
"mfa.no_key": "O início de sessão em dois passos ainda não está configurado neste servidor. Contacte o administrador.",
|
|
826
|
+
"mfa.app_name": "Nome",
|
|
827
|
+
"mfa.app_default": "Aplicação de autenticação",
|
|
828
|
+
"mfa.on": "Ao iniciar sessão, introduz também um código da sua aplicação de autenticação.",
|
|
829
|
+
"mfa.off": "Adicione uma aplicação de autenticação para proteger a sua conta com um segundo passo ao iniciar sessão.",
|
|
830
|
+
"mfa.required_you": "Esta aplicação exige-lhe o início de sessão em dois passos: adicione uma aplicação de autenticação.",
|
|
831
|
+
"mfa.add_app": "Adicionar uma aplicação de autenticação",
|
|
832
|
+
"mfa.confirm_app": "Adicionar aplicação",
|
|
833
|
+
"mfa.cancel": "Cancelar",
|
|
834
|
+
"mfa.remove": "Remover",
|
|
835
|
+
"mfa.remove_code": "Um código atual ou um código de recuperação",
|
|
836
|
+
"mfa.removed": "A aplicação de autenticação foi removida.",
|
|
837
|
+
"mfa.keep_one": "Esta aplicação exige-lhe o início de sessão em dois passos: adicione outra aplicação antes de remover esta.",
|
|
838
|
+
"mfa.added": "A aplicação de autenticação foi adicionada.",
|
|
839
|
+
"mfa.codes_left": "Códigos de recuperação restantes: {n}",
|
|
840
|
+
"mfa.codes_new": "Novos códigos de recuperação",
|
|
841
|
+
"mfa.recovery_used": "Iniciou sessão com um código de recuperação. Códigos de recuperação restantes: {n}.",
|
|
808
842
|
};
|
package/src/i18n/ru.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const ru: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "{what} по группам",
|
|
806
806
|
'ai_filter.needs_number': "Для {fn} нужен столбец с числами.",
|
|
807
807
|
'ai_filter.invalid': "Это изменение невозможно: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "Двухэтапный вход",
|
|
810
|
+
"mfa.step_intro": "Введите 6-значный код из приложения-аутентификатора или один из кодов восстановления.",
|
|
811
|
+
"mfa.code": "Код",
|
|
812
|
+
"mfa.verify": "Подтвердить",
|
|
813
|
+
"mfa.wrong": "Код неверен или уже был использован.",
|
|
814
|
+
"mfa.expired": "Вход занял слишком много времени. Войдите снова.",
|
|
815
|
+
"mfa.enrol_title": "Настройка двухэтапного входа",
|
|
816
|
+
"mfa.enrol_intro": "Это приложение при входе запрашивает код из приложения-аутентификатора. Добавьте его сейчас, чтобы продолжить.",
|
|
817
|
+
"mfa.scan": "Отсканируйте QR-код приложением-аутентификатором, например Google Authenticator, Microsoft Authenticator или 1Password.",
|
|
818
|
+
"mfa.or_enter": "Или введите этот ключ:",
|
|
819
|
+
"mfa.enter_code": "Введите 6-значный код, который показывает приложение.",
|
|
820
|
+
"mfa.qr_label": "QR-код для приложения-аутентификатора",
|
|
821
|
+
"mfa.codes_title": "Коды восстановления",
|
|
822
|
+
"mfa.codes_once": "Храните эти коды в надёжном месте. Каждый из них позволяет войти один раз, если у вас нет телефона. Они показываются только сейчас.",
|
|
823
|
+
"mfa.codes_saved": "Я сохранил эти коды",
|
|
824
|
+
"mfa.continue": "Продолжить",
|
|
825
|
+
"mfa.no_key": "Двухэтапный вход на этом сервере ещё не настроен. Обратитесь к администратору.",
|
|
826
|
+
"mfa.app_name": "Название",
|
|
827
|
+
"mfa.app_default": "Приложение-аутентификатор",
|
|
828
|
+
"mfa.on": "При входе вы также вводите код из приложения-аутентификатора.",
|
|
829
|
+
"mfa.off": "Добавьте приложение-аутентификатор, чтобы защитить учётную запись вторым шагом при входе.",
|
|
830
|
+
"mfa.required_you": "Это приложение требует от вас двухэтапного входа: добавьте приложение-аутентификатор.",
|
|
831
|
+
"mfa.add_app": "Добавить приложение-аутентификатор",
|
|
832
|
+
"mfa.confirm_app": "Добавить приложение",
|
|
833
|
+
"mfa.cancel": "Отмена",
|
|
834
|
+
"mfa.remove": "Удалить",
|
|
835
|
+
"mfa.remove_code": "Текущий код или код восстановления",
|
|
836
|
+
"mfa.removed": "Приложение-аутентификатор удалено.",
|
|
837
|
+
"mfa.keep_one": "Это приложение требует от вас двухэтапного входа: прежде чем удалить это приложение, добавьте другое.",
|
|
838
|
+
"mfa.added": "Приложение-аутентификатор добавлено.",
|
|
839
|
+
"mfa.codes_left": "Осталось кодов восстановления: {n}",
|
|
840
|
+
"mfa.codes_new": "Новые коды восстановления",
|
|
841
|
+
"mfa.recovery_used": "Вы вошли с кодом восстановления. Осталось кодов восстановления: {n}.",
|
|
808
842
|
};
|
package/src/i18n/sv.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const sv: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "{what} per grupp",
|
|
806
806
|
'ai_filter.needs_number': "{fn} kräver en kolumn med tal.",
|
|
807
807
|
'ai_filter.invalid': "Den ändringen går inte: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "Inloggning i två steg",
|
|
810
|
+
"mfa.step_intro": "Ange den 6-siffriga koden från din autentiseringsapp, eller en av dina återställningskoder.",
|
|
811
|
+
"mfa.code": "Kod",
|
|
812
|
+
"mfa.verify": "Verifiera",
|
|
813
|
+
"mfa.wrong": "Koden är fel eller har redan använts.",
|
|
814
|
+
"mfa.expired": "Inloggningen tog för lång tid. Logga in igen.",
|
|
815
|
+
"mfa.enrol_title": "Konfigurera inloggning i två steg",
|
|
816
|
+
"mfa.enrol_intro": "Det här programmet ber om en kod från en autentiseringsapp när du loggar in. Lägg till en nu för att fortsätta.",
|
|
817
|
+
"mfa.scan": "Skanna QR-koden med en autentiseringsapp, till exempel Google Authenticator, Microsoft Authenticator eller 1Password.",
|
|
818
|
+
"mfa.or_enter": "Eller ange den här nyckeln:",
|
|
819
|
+
"mfa.enter_code": "Ange den 6-siffriga kod som appen visar.",
|
|
820
|
+
"mfa.qr_label": "QR-kod för en autentiseringsapp",
|
|
821
|
+
"mfa.codes_title": "Återställningskoder",
|
|
822
|
+
"mfa.codes_once": "Förvara koderna på ett säkert ställe. Var och en loggar in dig en gång när du inte har telefonen. De visas bara nu.",
|
|
823
|
+
"mfa.codes_saved": "Jag har sparat koderna",
|
|
824
|
+
"mfa.continue": "Fortsätt",
|
|
825
|
+
"mfa.no_key": "Inloggning i två steg är inte konfigurerad på den här servern än. Kontakta administratören.",
|
|
826
|
+
"mfa.app_name": "Namn",
|
|
827
|
+
"mfa.app_default": "Autentiseringsapp",
|
|
828
|
+
"mfa.on": "När du loggar in anger du också en kod från din autentiseringsapp.",
|
|
829
|
+
"mfa.off": "Lägg till en autentiseringsapp för att skydda ditt konto med ett andra steg när du loggar in.",
|
|
830
|
+
"mfa.required_you": "Det här programmet kräver inloggning i två steg för dig: lägg till en autentiseringsapp.",
|
|
831
|
+
"mfa.add_app": "Lägg till en autentiseringsapp",
|
|
832
|
+
"mfa.confirm_app": "Lägg till app",
|
|
833
|
+
"mfa.cancel": "Avbryt",
|
|
834
|
+
"mfa.remove": "Ta bort",
|
|
835
|
+
"mfa.remove_code": "En aktuell kod eller en återställningskod",
|
|
836
|
+
"mfa.removed": "Autentiseringsappen togs bort.",
|
|
837
|
+
"mfa.keep_one": "Det här programmet kräver inloggning i två steg för dig: lägg till en annan app innan du tar bort den här.",
|
|
838
|
+
"mfa.added": "Autentiseringsappen lades till.",
|
|
839
|
+
"mfa.codes_left": "Återstående återställningskoder: {n}",
|
|
840
|
+
"mfa.codes_new": "Nya återställningskoder",
|
|
841
|
+
"mfa.recovery_used": "Du loggade in med en återställningskod. Återstående återställningskoder: {n}.",
|
|
808
842
|
};
|
package/src/i18n/tr.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const tr: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "grup başına {what}",
|
|
806
806
|
'ai_filter.needs_number': "{fn} sayı içeren bir sütun gerektirir.",
|
|
807
807
|
'ai_filter.invalid': "Bu değişiklik yapılamaz: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "İki adımlı oturum açma",
|
|
810
|
+
"mfa.step_intro": "Kimlik doğrulama uygulamanızdaki 6 haneli kodu veya kurtarma kodlarınızdan birini girin.",
|
|
811
|
+
"mfa.code": "Kod",
|
|
812
|
+
"mfa.verify": "Doğrula",
|
|
813
|
+
"mfa.wrong": "Kod yanlış veya daha önce kullanılmış.",
|
|
814
|
+
"mfa.expired": "Oturum açma çok uzun sürdü. Lütfen yeniden oturum açın.",
|
|
815
|
+
"mfa.enrol_title": "İki adımlı oturum açmayı ayarlayın",
|
|
816
|
+
"mfa.enrol_intro": "Bu uygulama oturum açarken bir kimlik doğrulama uygulamasından kod ister. Devam etmek için şimdi bir tane ekleyin.",
|
|
817
|
+
"mfa.scan": "QR kodunu Google Authenticator, Microsoft Authenticator veya 1Password gibi bir kimlik doğrulama uygulamasıyla tarayın.",
|
|
818
|
+
"mfa.or_enter": "Veya bu anahtarı girin:",
|
|
819
|
+
"mfa.enter_code": "Uygulamanın gösterdiği 6 haneli kodu girin.",
|
|
820
|
+
"mfa.qr_label": "Kimlik doğrulama uygulaması için QR kodu",
|
|
821
|
+
"mfa.codes_title": "Kurtarma kodları",
|
|
822
|
+
"mfa.codes_once": "Bu kodları güvenli bir yerde saklayın. Telefonunuz yanınızda olmadığında her biri bir kez oturum açmanızı sağlar. Yalnızca şimdi gösterilirler.",
|
|
823
|
+
"mfa.codes_saved": "Bu kodları kaydettim",
|
|
824
|
+
"mfa.continue": "Devam",
|
|
825
|
+
"mfa.no_key": "İki adımlı oturum açma bu sunucuda henüz ayarlanmadı. Lütfen yöneticiye başvurun.",
|
|
826
|
+
"mfa.app_name": "Ad",
|
|
827
|
+
"mfa.app_default": "Kimlik doğrulama uygulaması",
|
|
828
|
+
"mfa.on": "Oturum açarken kimlik doğrulama uygulamanızdan bir kod da girersiniz.",
|
|
829
|
+
"mfa.off": "Oturum açarken hesabınızı ikinci bir adımla korumak için bir kimlik doğrulama uygulaması ekleyin.",
|
|
830
|
+
"mfa.required_you": "Bu uygulama sizin için iki adımlı oturum açmayı zorunlu kılıyor: bir kimlik doğrulama uygulaması ekleyin.",
|
|
831
|
+
"mfa.add_app": "Kimlik doğrulama uygulaması ekle",
|
|
832
|
+
"mfa.confirm_app": "Uygulama ekle",
|
|
833
|
+
"mfa.cancel": "İptal",
|
|
834
|
+
"mfa.remove": "Kaldır",
|
|
835
|
+
"mfa.remove_code": "Geçerli bir kod veya kurtarma kodu",
|
|
836
|
+
"mfa.removed": "Kimlik doğrulama uygulaması kaldırıldı.",
|
|
837
|
+
"mfa.keep_one": "Bu uygulama sizin için iki adımlı oturum açmayı zorunlu kılıyor: bunu kaldırmadan önce başka bir uygulama ekleyin.",
|
|
838
|
+
"mfa.added": "Kimlik doğrulama uygulaması eklendi.",
|
|
839
|
+
"mfa.codes_left": "Kalan kurtarma kodları: {n}",
|
|
840
|
+
"mfa.codes_new": "Yeni kurtarma kodları",
|
|
841
|
+
"mfa.recovery_used": "Kurtarma koduyla oturum açtınız. Kalan kurtarma kodları: {n}.",
|
|
808
842
|
};
|
package/src/i18n/uk.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const uk: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "{what} за групами",
|
|
806
806
|
'ai_filter.needs_number': "Для {fn} потрібен стовпець із числами.",
|
|
807
807
|
'ai_filter.invalid': "Ця зміна неможлива: {why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "Двоетапний вхід",
|
|
810
|
+
"mfa.step_intro": "Введіть 6-значний код із застосунку-автентифікатора або один із кодів відновлення.",
|
|
811
|
+
"mfa.code": "Код",
|
|
812
|
+
"mfa.verify": "Підтвердити",
|
|
813
|
+
"mfa.wrong": "Код неправильний або вже був використаний.",
|
|
814
|
+
"mfa.expired": "Вхід тривав надто довго. Увійдіть знову.",
|
|
815
|
+
"mfa.enrol_title": "Налаштування двоетапного входу",
|
|
816
|
+
"mfa.enrol_intro": "Цей застосунок під час входу запитує код із застосунку-автентифікатора. Додайте його зараз, щоб продовжити.",
|
|
817
|
+
"mfa.scan": "Відскануйте QR-код застосунком-автентифікатором, наприклад Google Authenticator, Microsoft Authenticator або 1Password.",
|
|
818
|
+
"mfa.or_enter": "Або введіть цей ключ:",
|
|
819
|
+
"mfa.enter_code": "Введіть 6-значний код, який показує застосунок.",
|
|
820
|
+
"mfa.qr_label": "QR-код для застосунку-автентифікатора",
|
|
821
|
+
"mfa.codes_title": "Коди відновлення",
|
|
822
|
+
"mfa.codes_once": "Зберігайте ці коди в надійному місці. Кожен із них дає змогу увійти один раз, коли у вас немає телефона. Вони показуються лише зараз.",
|
|
823
|
+
"mfa.codes_saved": "Я зберіг ці коди",
|
|
824
|
+
"mfa.continue": "Продовжити",
|
|
825
|
+
"mfa.no_key": "Двоетапний вхід на цьому сервері ще не налаштовано. Зверніться до адміністратора.",
|
|
826
|
+
"mfa.app_name": "Назва",
|
|
827
|
+
"mfa.app_default": "Застосунок-автентифікатор",
|
|
828
|
+
"mfa.on": "Під час входу ви також вводите код із застосунку-автентифікатора.",
|
|
829
|
+
"mfa.off": "Додайте застосунок-автентифікатор, щоб захистити обліковий запис другим кроком під час входу.",
|
|
830
|
+
"mfa.required_you": "Цей застосунок вимагає від вас двоетапного входу: додайте застосунок-автентифікатор.",
|
|
831
|
+
"mfa.add_app": "Додати застосунок-автентифікатор",
|
|
832
|
+
"mfa.confirm_app": "Додати застосунок",
|
|
833
|
+
"mfa.cancel": "Скасувати",
|
|
834
|
+
"mfa.remove": "Вилучити",
|
|
835
|
+
"mfa.remove_code": "Поточний код або код відновлення",
|
|
836
|
+
"mfa.removed": "Застосунок-автентифікатор вилучено.",
|
|
837
|
+
"mfa.keep_one": "Цей застосунок вимагає від вас двоетапного входу: перш ніж вилучити цей застосунок, додайте інший.",
|
|
838
|
+
"mfa.added": "Застосунок-автентифікатор додано.",
|
|
839
|
+
"mfa.codes_left": "Залишилося кодів відновлення: {n}",
|
|
840
|
+
"mfa.codes_new": "Нові коди відновлення",
|
|
841
|
+
"mfa.recovery_used": "Ви увійшли з кодом відновлення. Залишилося кодів відновлення: {n}.",
|
|
808
842
|
};
|
package/src/i18n/zh.ts
CHANGED
|
@@ -805,4 +805,38 @@ export const zh: Record<MessageKey, string> = {
|
|
|
805
805
|
'ai_filter.per_group': "每组 {what}",
|
|
806
806
|
'ai_filter.needs_number': "{fn} 需要数字列。",
|
|
807
807
|
'ai_filter.invalid': "无法进行此更改:{why}",
|
|
808
|
+
// (sprint 130) two-step sign-in
|
|
809
|
+
"mfa.step_title": "两步登录",
|
|
810
|
+
"mfa.step_intro": "请输入身份验证器应用中的 6 位代码,或你的一个恢复代码。",
|
|
811
|
+
"mfa.code": "代码",
|
|
812
|
+
"mfa.verify": "验证",
|
|
813
|
+
"mfa.wrong": "代码不正确,或已被使用。",
|
|
814
|
+
"mfa.expired": "登录耗时过长。请重新登录。",
|
|
815
|
+
"mfa.enrol_title": "设置两步登录",
|
|
816
|
+
"mfa.enrol_intro": "此应用程序在登录时需要身份验证器应用中的代码。请立即添加一个以继续。",
|
|
817
|
+
"mfa.scan": "使用身份验证器应用(如 Google Authenticator、Microsoft Authenticator 或 1Password)扫描二维码。",
|
|
818
|
+
"mfa.or_enter": "或输入此密钥:",
|
|
819
|
+
"mfa.enter_code": "输入应用显示的 6 位代码。",
|
|
820
|
+
"mfa.qr_label": "用于身份验证器应用的二维码",
|
|
821
|
+
"mfa.codes_title": "恢复代码",
|
|
822
|
+
"mfa.codes_once": "请将这些代码保存在安全的地方。没有手机时,每个代码可让你登录一次。它们只会显示这一次。",
|
|
823
|
+
"mfa.codes_saved": "我已保存这些代码",
|
|
824
|
+
"mfa.continue": "继续",
|
|
825
|
+
"mfa.no_key": "此服务器尚未设置两步登录。请联系管理员。",
|
|
826
|
+
"mfa.app_name": "名称",
|
|
827
|
+
"mfa.app_default": "身份验证器应用",
|
|
828
|
+
"mfa.on": "登录时,你还需要输入身份验证器应用中的代码。",
|
|
829
|
+
"mfa.off": "添加身份验证器应用,在登录时用第二步保护你的账户。",
|
|
830
|
+
"mfa.required_you": "此应用程序要求你使用两步登录:请添加身份验证器应用。",
|
|
831
|
+
"mfa.add_app": "添加身份验证器应用",
|
|
832
|
+
"mfa.confirm_app": "添加应用",
|
|
833
|
+
"mfa.cancel": "取消",
|
|
834
|
+
"mfa.remove": "移除",
|
|
835
|
+
"mfa.remove_code": "当前代码或恢复代码",
|
|
836
|
+
"mfa.removed": "已移除身份验证器应用。",
|
|
837
|
+
"mfa.keep_one": "此应用程序要求你使用两步登录:请先添加另一个应用,再移除此应用。",
|
|
838
|
+
"mfa.added": "已添加身份验证器应用。",
|
|
839
|
+
"mfa.codes_left": "剩余恢复代码:{n}",
|
|
840
|
+
"mfa.codes_new": "新的恢复代码",
|
|
841
|
+
"mfa.recovery_used": "你使用恢复代码登录了。剩余恢复代码:{n}。",
|
|
808
842
|
};
|
package/src/i18n.ts
CHANGED
|
@@ -832,6 +832,40 @@ const en = {
|
|
|
832
832
|
'ai_filter.per_group': "{what} per group",
|
|
833
833
|
'ai_filter.needs_number': "{fn} needs a column with numbers.",
|
|
834
834
|
'ai_filter.invalid': "That change is not possible: {why}",
|
|
835
|
+
// (sprint 130) two-factor sign-in
|
|
836
|
+
"mfa.step_title": "Two-factor sign-in",
|
|
837
|
+
"mfa.step_intro": "Enter the 6-digit code from your authenticator app, or one of your recovery codes.",
|
|
838
|
+
"mfa.code": "Code",
|
|
839
|
+
"mfa.verify": "Verify",
|
|
840
|
+
"mfa.wrong": "That code is not right, or it was used already.",
|
|
841
|
+
"mfa.expired": "Signing in took too long. Please sign in again.",
|
|
842
|
+
"mfa.enrol_title": "Set up two-factor sign-in",
|
|
843
|
+
"mfa.enrol_intro": "This application asks for a code from an authenticator app when you sign in. Add one now to continue.",
|
|
844
|
+
"mfa.scan": "Scan the QR code with an authenticator app, such as Google Authenticator, Microsoft Authenticator or 1Password.",
|
|
845
|
+
"mfa.or_enter": "Or enter this key:",
|
|
846
|
+
"mfa.enter_code": "Enter the 6-digit code the app shows.",
|
|
847
|
+
"mfa.qr_label": "QR code for an authenticator app",
|
|
848
|
+
"mfa.codes_title": "Recovery codes",
|
|
849
|
+
"mfa.codes_once": "Keep these codes in a safe place. Each one signs you in once when you don't have your phone. They are shown only now.",
|
|
850
|
+
"mfa.codes_saved": "I have saved these codes",
|
|
851
|
+
"mfa.continue": "Continue",
|
|
852
|
+
"mfa.no_key": "Two-factor sign-in is not set up on this server yet. Please ask the administrator.",
|
|
853
|
+
"mfa.app_name": "Name",
|
|
854
|
+
"mfa.app_default": "Authenticator app",
|
|
855
|
+
"mfa.on": "When you sign in, you also give a code from your authenticator app.",
|
|
856
|
+
"mfa.off": "Add an authenticator app to protect your account with a second step when you sign in.",
|
|
857
|
+
"mfa.required_you": "This application requires two-factor sign-in for you: add an authenticator app.",
|
|
858
|
+
"mfa.add_app": "Add an authenticator app",
|
|
859
|
+
"mfa.confirm_app": "Add app",
|
|
860
|
+
"mfa.cancel": "Cancel",
|
|
861
|
+
"mfa.remove": "Remove",
|
|
862
|
+
"mfa.remove_code": "A current code or a recovery code",
|
|
863
|
+
"mfa.removed": "The authenticator app was removed.",
|
|
864
|
+
"mfa.keep_one": "This application requires two-factor sign-in for you: add another app before you remove this one.",
|
|
865
|
+
"mfa.added": "The authenticator app was added.",
|
|
866
|
+
"mfa.codes_left": "Recovery codes left: {n}",
|
|
867
|
+
"mfa.codes_new": "New recovery codes",
|
|
868
|
+
"mfa.recovery_used": "You signed in with a recovery code. Recovery codes left: {n}.",
|
|
835
869
|
};
|
|
836
870
|
|
|
837
871
|
export type MessageKey = keyof typeof en;
|
|
@@ -1633,6 +1667,40 @@ const nl: Record<MessageKey, string> = {
|
|
|
1633
1667
|
'ai_filter.per_group': "{what} per groep",
|
|
1634
1668
|
'ai_filter.needs_number': "{fn} vraagt een kolom met getallen.",
|
|
1635
1669
|
'ai_filter.invalid': "Die wijziging kan niet: {why}",
|
|
1670
|
+
// (sprint 130) two-factor sign-in
|
|
1671
|
+
"mfa.step_title": "Aanmelden in twee stappen",
|
|
1672
|
+
"mfa.step_intro": "Voer de 6-cijferige code uit je authenticator-app in, of een van je herstelcodes.",
|
|
1673
|
+
"mfa.code": "Code",
|
|
1674
|
+
"mfa.verify": "Controleren",
|
|
1675
|
+
"mfa.wrong": "Die code klopt niet, of is al gebruikt.",
|
|
1676
|
+
"mfa.expired": "Het aanmelden duurde te lang. Meld je opnieuw aan.",
|
|
1677
|
+
"mfa.enrol_title": "Aanmelden in twee stappen instellen",
|
|
1678
|
+
"mfa.enrol_intro": "Deze applicatie vraagt bij het aanmelden om een code uit een authenticator-app. Voeg er nu een toe om verder te gaan.",
|
|
1679
|
+
"mfa.scan": "Scan de QR-code met een authenticator-app, zoals Google Authenticator, Microsoft Authenticator of 1Password.",
|
|
1680
|
+
"mfa.or_enter": "Of voer deze sleutel in:",
|
|
1681
|
+
"mfa.enter_code": "Voer de 6-cijferige code in die de app toont.",
|
|
1682
|
+
"mfa.qr_label": "QR-code voor een authenticator-app",
|
|
1683
|
+
"mfa.codes_title": "Herstelcodes",
|
|
1684
|
+
"mfa.codes_once": "Bewaar deze codes op een veilige plek. Met elke code meld je je één keer aan als je je telefoon niet bij je hebt. Ze worden alleen nu getoond.",
|
|
1685
|
+
"mfa.codes_saved": "Ik heb deze codes bewaard",
|
|
1686
|
+
"mfa.continue": "Doorgaan",
|
|
1687
|
+
"mfa.no_key": "Aanmelden in twee stappen is op deze server nog niet ingesteld. Vraag het de beheerder.",
|
|
1688
|
+
"mfa.app_name": "Naam",
|
|
1689
|
+
"mfa.app_default": "Authenticator-app",
|
|
1690
|
+
"mfa.on": "Bij het aanmelden geef je ook een code uit je authenticator-app.",
|
|
1691
|
+
"mfa.off": "Voeg een authenticator-app toe om je account bij het aanmelden met een tweede stap te beschermen.",
|
|
1692
|
+
"mfa.required_you": "Deze applicatie vereist voor jou aanmelden in twee stappen: voeg een authenticator-app toe.",
|
|
1693
|
+
"mfa.add_app": "Authenticator-app toevoegen",
|
|
1694
|
+
"mfa.confirm_app": "App toevoegen",
|
|
1695
|
+
"mfa.cancel": "Annuleren",
|
|
1696
|
+
"mfa.remove": "Verwijderen",
|
|
1697
|
+
"mfa.remove_code": "Een actuele code of een herstelcode",
|
|
1698
|
+
"mfa.removed": "De authenticator-app is verwijderd.",
|
|
1699
|
+
"mfa.keep_one": "Deze applicatie vereist voor jou aanmelden in twee stappen: voeg eerst een andere app toe voordat je deze verwijdert.",
|
|
1700
|
+
"mfa.added": "De authenticator-app is toegevoegd.",
|
|
1701
|
+
"mfa.codes_left": "Resterende herstelcodes: {n}",
|
|
1702
|
+
"mfa.codes_new": "Nieuwe herstelcodes",
|
|
1703
|
+
"mfa.recovery_used": "Je hebt je aangemeld met een herstelcode. Resterende herstelcodes: {n}.",
|
|
1636
1704
|
};
|
|
1637
1705
|
|
|
1638
1706
|
const BUILTIN: Record<string, Record<string, string>> = { en, nl, de, fr, es, it, pt, pl, sv, da, nb, cs, ja, zh, fi, tr, el, ru, uk, ko, ar, he };
|
package/src/metadata.ts
CHANGED
|
@@ -78,6 +78,9 @@ export interface App {
|
|
|
78
78
|
password_reset: boolean;
|
|
79
79
|
/** (067) session sharing group (src/sharedlogin.ts) */
|
|
80
80
|
session_group: string | null;
|
|
81
|
+
/** (132) two-step sign-in for the application's users (src/account-mfa.ts) */
|
|
82
|
+
mfa: 'off' | 'optional' | 'required' | 'roles';
|
|
83
|
+
mfa_roles: string[];
|
|
81
84
|
/** LDAP directories the password form checks after local accounts */
|
|
82
85
|
ldap_directories: string[];
|
|
83
86
|
/** Progressive Web App: installable, offline pages, offline form queue */
|
|
@@ -369,7 +372,7 @@ const agg = (table: string, fk: string, parent: string, appId: string) =>
|
|
|
369
372
|
// No caching on purpose: edits made in the builder show up on the next request.
|
|
370
373
|
export async function loadApp(alias: string) {
|
|
371
374
|
return runtime.one<App>(
|
|
372
|
-
`select a.id, a.alias, a.name, a.version, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
|
|
375
|
+
`select a.id, a.alias, a.name, a.version, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.mfa, a.mfa_roles, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
|
|
373
376
|
a.availability, a.availability_message, a.availability_users, a.feedback, a.error_function, a.session_warning_seconds, a.component_settings,
|
|
374
377
|
a.language, a.languages, a.language_from, a.date_format, a.timestamp_format, a.time_zone, a.time_zone_auto, a.currency,
|
|
375
378
|
coalesce((select jsonb_agg(jsonb_build_object('name', l.name, 'query', l.query, 'rest_source', l.rest_source)) from meta.lov l where l.app_id = a.id), '[]') as lovs,
|
package/src/mfa.ts
CHANGED
|
@@ -94,6 +94,75 @@ export function newRecoveryCode(): string {
|
|
|
94
94
|
export const normaliseCode = (code: string) => code.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
95
95
|
export const hashCode = (code: string) => createHash('sha256').update(normaliseCode(code)).digest('hex');
|
|
96
96
|
|
|
97
|
+
// ---------------------------------------------------------------- where factors are kept
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* (sprint 130, SEC-3) The tables of one kind of user: developers (128, by user name) or application accounts
|
|
101
|
+
* (132, by account id). The code checks, recovery codes and the "each step once" rule are the same for both.
|
|
102
|
+
*/
|
|
103
|
+
export interface FactorStore { factors: string; codes: string; key: string }
|
|
104
|
+
export const DEVELOPERS: FactorStore = { factors: 'meta.developer_factor', codes: 'meta.developer_recovery_code', key: 'username' };
|
|
105
|
+
export const ACCOUNTS: FactorStore = { factors: 'meta.account_factor', codes: 'meta.account_recovery_code', key: 'account_id' };
|
|
106
|
+
type Who = string | number;
|
|
107
|
+
|
|
108
|
+
export async function storeCodesLeft(st: FactorStore, who: Who): Promise<number> {
|
|
109
|
+
return (await owner.one<{ n: number }>(`select count(*)::int as n from ${st.codes} where ${st.key} = $1 and confirmed and used_at is null`, [who]))!.n;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function storeHasFactor(st: FactorStore, who: Who): Promise<boolean> {
|
|
113
|
+
return !!(await owner.one(`select 1 as ok from ${st.factors} where ${st.key} = $1`, [who]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function storeAddTotp(st: FactorStore, who: Who, name: string, secret: string, step: number) {
|
|
117
|
+
await owner.query(
|
|
118
|
+
`insert into ${st.factors} (${st.key}, kind, name, secret_enc, last_step, last_used_at) values ($1, 'totp', $2, $3, $4, now())`,
|
|
119
|
+
[who, name.trim().slice(0, 60) || 'Authenticator', encryptSecret(secret), step],
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function storeNewCodes(st: FactorStore, who: Who): Promise<string[]> {
|
|
124
|
+
const codes = Array.from({ length: RECOVERY_CODES }, newRecoveryCode);
|
|
125
|
+
await owner.tx(async (c) => {
|
|
126
|
+
await c.query(`delete from ${st.codes} where ${st.key} = $1`, [who]);
|
|
127
|
+
for (const code of codes) await c.query(`insert into ${st.codes} (${st.key}, code_hash) values ($1, $2)`, [who, hashCode(code)]);
|
|
128
|
+
});
|
|
129
|
+
return codes;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function storeConfirmCodes(st: FactorStore, who: Who) {
|
|
133
|
+
await owner.query(`update ${st.codes} set confirmed = true where ${st.key} = $1 and used_at is null`, [who]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check a second-factor code: an authenticator code (each step once) or a confirmed, unused recovery code
|
|
138
|
+
* (then used up). Returns what matched, or null.
|
|
139
|
+
*/
|
|
140
|
+
export async function storeCheck(st: FactorStore, who: Who, code: string): Promise<{ kind: 'totp' | 'recovery'; name?: string } | null> {
|
|
141
|
+
const digits = code.replace(/\s/g, '');
|
|
142
|
+
if (/^\d{6}$/.test(digits)) {
|
|
143
|
+
const rows = (await owner.query<{ id: number; name: string; secret_enc: string; last_step: string | null }>(
|
|
144
|
+
`select id, name, secret_enc, last_step from ${st.factors} where ${st.key} = $1 and kind = 'totp'`, [who])).rows;
|
|
145
|
+
for (const f of rows) {
|
|
146
|
+
let secret: Buffer;
|
|
147
|
+
try {
|
|
148
|
+
secret = unbase32(decryptSecret(f.secret_enc));
|
|
149
|
+
} catch {
|
|
150
|
+
continue; // the instance key changed: recovery codes still work
|
|
151
|
+
}
|
|
152
|
+
const step = matchTotp(secret, digits, f.last_step === null ? null : Number(f.last_step));
|
|
153
|
+
// the update only wins once per step, also with two requests at the same time
|
|
154
|
+
if (step !== null && (await owner.query(`update ${st.factors} set last_step = $2, last_used_at = now() where id = $1 and (last_step is null or last_step < $2)`, [f.id, step])).rowCount)
|
|
155
|
+
return { kind: 'totp', name: f.name };
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (normaliseCode(code).length !== 10) return null;
|
|
160
|
+
const used = await owner.query(
|
|
161
|
+
`update ${st.codes} set used_at = now() where ${st.key} = $1 and code_hash = $2 and confirmed and used_at is null`,
|
|
162
|
+
[who, hashCode(code)]);
|
|
163
|
+
return used.rowCount ? { kind: 'recovery' } : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
97
166
|
// ---------------------------------------------------------------- the developer's factors
|
|
98
167
|
|
|
99
168
|
export interface Factor { id: number; kind: 'totp' | 'passkey'; name: string; created_at: string; last_used_at: string | null; rp_id: string | null }
|
|
@@ -103,22 +172,13 @@ export async function factorsOf(username: string, db: Pick<pg.ClientBase, 'query
|
|
|
103
172
|
}
|
|
104
173
|
|
|
105
174
|
/** Recovery codes left (confirmed, unused). */
|
|
106
|
-
export
|
|
107
|
-
return (await owner.one<{ n: number }>('select count(*)::int as n from meta.developer_recovery_code where username = $1 and confirmed and used_at is null', [username]))!.n;
|
|
108
|
-
}
|
|
175
|
+
export const codesLeft = (username: string) => storeCodesLeft(DEVELOPERS, username);
|
|
109
176
|
|
|
110
177
|
/** Whether the developer has a working second factor (an authenticator app; recovery codes back it up). */
|
|
111
|
-
export
|
|
112
|
-
return !!(await owner.one('select 1 as ok from meta.developer_factor where username = $1', [username]));
|
|
113
|
-
}
|
|
178
|
+
export const hasFactor = (username: string) => storeHasFactor(DEVELOPERS, username);
|
|
114
179
|
|
|
115
180
|
/** Add an authenticator app (after the developer confirmed a code from it). */
|
|
116
|
-
export
|
|
117
|
-
await owner.query(
|
|
118
|
-
`insert into meta.developer_factor (username, kind, name, secret_enc, last_step, last_used_at) values ($1, 'totp', $2, $3, $4, now())`,
|
|
119
|
-
[username, name.trim().slice(0, 60) || 'Authenticator', encryptSecret(secret), step],
|
|
120
|
-
);
|
|
121
|
-
}
|
|
181
|
+
export const addTotp = (username: string, name: string, secret: string, step: number) => storeAddTotp(DEVELOPERS, username, name, secret, step);
|
|
122
182
|
|
|
123
183
|
// ---------------------------------------------------------------- passkeys (sprint 127)
|
|
124
184
|
|
|
@@ -211,48 +271,12 @@ export async function forgetBrowsers(username: string) {
|
|
|
211
271
|
}
|
|
212
272
|
|
|
213
273
|
/** New recovery codes (the old ones stop working); shown once, counted only when confirmed. */
|
|
214
|
-
export
|
|
215
|
-
const codes = Array.from({ length: RECOVERY_CODES }, newRecoveryCode);
|
|
216
|
-
await owner.tx(async (c) => {
|
|
217
|
-
await c.query('delete from meta.developer_recovery_code where username = $1', [username]);
|
|
218
|
-
for (const code of codes) await c.query('insert into meta.developer_recovery_code (username, code_hash) values ($1, $2)', [username, hashCode(code)]);
|
|
219
|
-
});
|
|
220
|
-
return codes;
|
|
221
|
-
}
|
|
274
|
+
export const newRecoveryCodes = (username: string) => storeNewCodes(DEVELOPERS, username);
|
|
222
275
|
|
|
223
|
-
export
|
|
224
|
-
await owner.query('update meta.developer_recovery_code set confirmed = true where username = $1 and used_at is null', [username]);
|
|
225
|
-
}
|
|
276
|
+
export const confirmRecoveryCodes = (username: string) => storeConfirmCodes(DEVELOPERS, username);
|
|
226
277
|
|
|
227
|
-
/**
|
|
228
|
-
|
|
229
|
-
* recovery code (then used up). Returns what matched, or null.
|
|
230
|
-
*/
|
|
231
|
-
export async function checkSecondFactor(username: string, code: string): Promise<{ kind: 'totp' | 'recovery'; name?: string } | null> {
|
|
232
|
-
const digits = code.replace(/\s/g, '');
|
|
233
|
-
if (/^\d{6}$/.test(digits)) {
|
|
234
|
-
const rows = (await owner.query<{ id: number; name: string; secret_enc: string; last_step: string | null }>(
|
|
235
|
-
'select id, name, secret_enc, last_step from meta.developer_factor where username = $1 and kind = $2', [username, 'totp'])).rows;
|
|
236
|
-
for (const f of rows) {
|
|
237
|
-
let secret: Buffer;
|
|
238
|
-
try {
|
|
239
|
-
secret = unbase32(decryptSecret(f.secret_enc));
|
|
240
|
-
} catch {
|
|
241
|
-
continue; // the instance key changed: recovery codes still work
|
|
242
|
-
}
|
|
243
|
-
const step = matchTotp(secret, digits, f.last_step === null ? null : Number(f.last_step));
|
|
244
|
-
// the update only wins once per step, also with two requests at the same time
|
|
245
|
-
if (step !== null && (await owner.query('update meta.developer_factor set last_step = $2, last_used_at = now() where id = $1 and (last_step is null or last_step < $2)', [f.id, step])).rowCount)
|
|
246
|
-
return { kind: 'totp', name: f.name };
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
if (normaliseCode(code).length !== 10) return null;
|
|
251
|
-
const used = await owner.query(
|
|
252
|
-
'update meta.developer_recovery_code set used_at = now() where username = $1 and code_hash = $2 and confirmed and used_at is null',
|
|
253
|
-
[username, hashCode(code)]);
|
|
254
|
-
return used.rowCount ? { kind: 'recovery' } : null;
|
|
255
|
-
}
|
|
278
|
+
/** Check a developer's second-factor code at sign-in (see storeCheck). */
|
|
279
|
+
export const checkSecondFactor = (username: string, code: string) => storeCheck(DEVELOPERS, username, code);
|
|
256
280
|
|
|
257
281
|
// ---------------------------------------------------------------- policy
|
|
258
282
|
|