diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..333d29757
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+dev_database
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..1d8bc6e4d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{go,sh}]
+indent_style = tab
+
+[{Makefile,Caddyfile}]
+indent_style = tab
+
+[LICENSE]
+trim_trailing_whitespace = false
+insert_final_newline = false
diff --git a/.gitignore b/.gitignore
index 18532909f..6190866a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@
# Dependency directories (remove the comment below to include it)
# vendor/
+dev_database/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..58279259a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,33 @@
+FROM node:22-alpine AS node
+WORKDIR /app
+
+# Download the node dependencies first before adding the rest for caching
+COPY ./net/web/package.json ./net/web/yarn.lock ./
+RUN yarn --frozen-lockfile
+
+COPY ./net/web/ ./
+RUN yarn run build
+
+FROM golang:alpine AS go
+EXPOSE 7000
+WORKDIR /app/databag
+
+RUN apk add build-base imagemagick sqlite ffmpeg curl
+
+RUN mkdir -p /opt/databag
+RUN mkdir -p /var/lib/databag
+RUN mkdir -p /app/databag/net
+
+COPY ./net/server /app/databag/net/server
+COPY ./net/transform /opt/databag/transform
+
+WORKDIR /app/databag/net/server
+RUN go mod download
+RUN CGO_ENABLED=1 go build -o databag .
+
+COPY --from=node /app/build /app/databag/net/web/build
+
+ENV DEV=0
+ENV ADMIN=password
+
+ENTRYPOINT /app/databag/net/server/entrypoint.sh
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..dccbceeb8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+default:
+ @grep '^[^#[:space:].].*:' Makefile
+
+dev-start:
+ docker compose -f docker-compose.dev.yml up -d
+dev-stop:
+ docker compose -f docker-compose.dev.yml down
+
+dev-restart-server:
+ docker compose -f docker-compose.dev.yml restart net-server
+dev-restart-web:
+ docker compose -f docker-compose.dev.yml restart net-web
+dev-restart-repeater:
+ docker compose -f docker-compose.dev.yml restart net-repeater
+
+prod-docker-start:
+ docker compose up -d
+
+prod-raw-build:
+ ./build.sh
diff --git a/README.md b/README.md
index 3cd0e8278..350ebe48b 100644
--- a/README.md
+++ b/README.md
@@ -56,10 +56,24 @@ The app is available on fdroid as well as the google and apple stores. You can t
To use databag, you will need a DNS name pointing to your node with a certificate. You can deploy a node manually, but you will have a much easier time using a container service. Containers for arm64 and amd64 are available [here](https://hub.docker.com/r/balzack/databag/tags).
-### Docker Compose Command
+### Docker Compose
-From the net/container sub directory:
- - sudo docker-compose -f compose.yaml -p databag up
+Launch with dockerhub container using docker compose:
+
+#### Standard launch
+```shell
+# From the net/container sub directory:
+docker-compose -f compose.yaml -p databag up
+```
+
+#### Launch with certbot https certificate
+```shell
+# FIRST: create a DNS entry in your DNS to point your desired subdomain to your host
+# SECOND: edit the net/container/docker-compose-swag.yml to include your domain name
+# THIRD: From the root of the project directory:
+mkdir -p ~/appdata
+docker-compose -f net/container/docker-compose-swag.yml -p databag up
+```
### Example with Portainer and Nginx Proxy Manager
@@ -154,4 +168,4 @@ If you want to enable audio and video calls, you should setup your own relay ser
### Roadmap
-Please let me know any missing features; [here](/doc/backlog.md) is the current backlog. Features are prioritized based on interest from the community.
+Please add any missing features; [here](/doc/backlog.md) is the current backlog. Features are prioritized based on interest from the community.
diff --git a/app/mobile/.gitignore b/app/mobile/.gitignore
new file mode 100644
index 000000000..fd4f2b066
--- /dev/null
+++ b/app/mobile/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.DS_Store
diff --git a/app/mobile/src/api/setCardStatus.js b/app/mobile/src/api/setCardStatus.js
index 975b8a4df..0b4238cb3 100644
--- a/app/mobile/src/api/setCardStatus.js
+++ b/app/mobile/src/api/setCardStatus.js
@@ -10,12 +10,18 @@ export async function setCardConnecting(server, token, cardId) {
}
export async function setCardConnected(server, token, cardId, access, view, article, channel, profile) {
+ const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
+ const protocol = insecure ? 'http' : 'https';
+
let card = await fetchWithTimeout(`${protocol}://${server}/contact/cards/${cardId}/status?agent=${token}&token=${access}&viewRevision=${view}&articleRevision=${article}&channelRevision=${channel}&profileRevision=${profile}`, { method: 'PUT', body: JSON.stringify('connected') } );
checkResponse(card);
return await card.json();
}
export async function setCardConfirmed(server, token, cardId) {
+ const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
+ const protocol = insecure ? 'http' : 'https';
+
let card = await fetchWithTimeout(`${protocol}://${server}/contact/cards/${cardId}/status?agent=${token}`, { method: 'PUT', body: JSON.stringify('confirmed') } );
checkResponse(card);
return await card.json();
diff --git a/app/mobile/src/constants/Strings.js b/app/mobile/src/constants/Strings.js
index 3a8b25aac..07209328c 100644
--- a/app/mobile/src/constants/Strings.js
+++ b/app/mobile/src/constants/Strings.js
@@ -206,7 +206,7 @@ const Strings = [
selectTopic: 'Select Topic for Sharing',
mfaTitle: 'Multi-Factor Authentication',
- mfaSteps: 'Store the SHA256 secret and confirm the verification code',
+ mfaSteps: 'Store the secret and confirm the verification code',
mfaError: 'verification code error',
mfaDisabled: 'verification temporarily disabled',
mfaConfirm: 'Confirm',
@@ -417,7 +417,7 @@ const Strings = [
selectTopic: 'Choisissez le sujet à partager',
mfaTitle: 'Authentification Multi-Factor',
- mfaSteps: 'Enregistrez le secret SHA256 et confirmez le code de vérification',
+ mfaSteps: 'Enregistrez le secret et confirmez le code de vérification',
mfaEnter: 'Entrez votre code de vérification',
mfaError: 'erreur de code de vérification',
mfaDisabled: 'vérification temporairement désactivée',
@@ -629,7 +629,7 @@ const Strings = [
selectTopic: 'Elija un tema para compartir',
mfaTitle: 'Autenticación de Dos Factores',
- mfaSteps: 'Guarde el secreto SHA256 y confirme el código de verificación',
+ mfaSteps: 'Guarde el secreto y confirme el código de verificación',
mfaEnter: 'Ingresa tu código de verificación',
mfaError: 'error de código de verificación',
mfaDisabled: 'verificación temporalmente deshabilitada',
@@ -641,18 +641,18 @@ const Strings = [
},
{
languageCode: 'de',
- visibleRegistry: 'Sichtbar in der Registrierung',
+ visibleRegistry: 'Im Verzeichnis sichtbar',
edit: 'Bearbeiten',
enableNotifications: 'Mitteilungen',
- allowUnsealed: 'Unsichere Themen',
- sealedTopics: 'Gesicherte Themen',
- colorMode: 'Farmodus',
+ allowUnsealed: 'Unversiegelte Themen ermöglichen',
+ sealedTopics: 'Versiegelte Themen',
+ colorMode: 'Farbmodus',
hourMode: 'Stunde',
dateMode: 'Datum',
language: 'Sprache',
logout: 'Ausloggen',
- changeLogin: 'Kennwort Aktualisieren',
- deleteAccount: 'Konto Löschen',
+ changeLogin: 'Kennwort aktualisieren',
+ deleteAccount: 'Konto löschen',
contacts: 'Kontakte',
topics: 'Themen',
messages: 'Mitteilungen',
@@ -663,8 +663,8 @@ const Strings = [
messages: 'Nachrichtenübermittlung',
timeFull: '24h',
timeHalf: '12h',
- monthStart: 'mm/dd',
- monthEnd: 'dd/mm',
+ monthStart: 'mm/tt',
+ monthEnd: 'tt/mm',
error: 'Fehler',
tryAgain: 'Bitte versuche es erneut.',
@@ -681,33 +681,33 @@ const Strings = [
removeSeal: 'Sicherheitsschlüssel entfernen',
disableSeal: 'Sicherheitsschlüssel deaktivieren',
unlockSeal: 'Sicherheitsschlüssel entsperren',
- typeDelete: 'Geben Sie [löschen]',
+ typeDelete: 'Bitte geben Sie [löschen] ein',
deleteKey: 'löschen',
- enableTopics: 'Aktivieren Sie gesicherte Themen',
- manageTopics: 'Sicherheitsschlüssel verwalten',
+ enableTopics: 'Aktivieren Sie versiegelte Themen',
+ manageTopics: 'Versiegelungsschlüssel verwalten',
changePassword: 'Ändern Sie das Passwort des Sicherheitsschlüssels',
update: 'Aktualisieren',
changeKey: 'Schlüsselpasswort ändern',
delayMessage: 'Die Schlüsselgenerierung kann mehrere Minuten dauern.',
changeMessage: 'Hier können Sie den Benutzernamen und/oder das Passwort für Ihr Konto ändern.',
- cancel: 'Stornieren',
+ cancel: 'Abbrechen',
confirmlogout: 'Ausloggen',
loggingOut: 'Abmelden bestätigen',
username: 'Nutzername',
save: 'Speichern',
- notAvailable: 'Benutzername Nicht Verfügbar',
+ notAvailable: 'Benutzername nicht verfügbar',
blockedContacts: 'Blockierte Kontakte',
- restoreContact: 'Kontakt Wiederherstellen?',
+ restoreContact: 'Kontakt wiederherstellen?',
blockedTopics: 'Blockierte Themen',
- restoreTopic: 'Thema Wiederherstellen?',
+ restoreTopic: 'Thema wiederherstellen?',
blockedMessages: 'Blockierte Nachrichten',
- restoreMessage: 'Nachricht Wiederherstellen?',
+ restoreMessage: 'Nachricht wiederherstellen?',
close: 'Schließen',
ok: 'OK',
- noBlockedContacts: 'Keine Blockierten Kontakte',
- noBlockedTopics: 'Keine Blockierten Themen',
- noBlockedMessages: 'Keine Blockierten Nachrichten',
+ noBlockedContacts: 'Keine blockierten Kontakte',
+ noBlockedTopics: 'Keine blockierten Themen',
+ noBlockedMessages: 'Keine blockierten Nachrichten',
restore: 'Wiederherstellen',
//profile page
@@ -715,20 +715,20 @@ const Strings = [
name: 'Name',
location: 'Standort',
description: 'Beschreibung',
- registryVisible: 'Sichtbar in der Registrierung',
+ registryVisible: 'Im Verzeichnis sichtbar',
editImage: 'Bild Bearbeiten',
editDetails: 'Details Bearbeiten',
//contacts page
- back: 'Rückwärts',
- deleteContact: 'Kontakt Löschen',
+ back: 'Zurück',
+ deleteContact: 'Kontakt löschen',
confirmDelete: 'Löschen',
- disconnectContact: 'Kontakt Trennen',
+ disconnectContact: 'Kontakt trennen',
confirmDisconnect: 'Trennen',
- blockContact: 'Kontakt Ausblenden',
- confirmBlock: 'Verstecken',
- reportContact: 'Kontakt Melden',
- confirmReport: 'Bericht',
+ blockContact: 'Kontakt blockieren',
+ confirmBlock: 'Blockieren',
+ reportContact: 'Kontakt melden',
+ confirmReport: 'Melden',
confirmed: 'Gerettet',
pending: 'Unbekannt',
connecting: 'Verbinden',
@@ -740,20 +740,20 @@ const Strings = [
actionConnect: 'Verbinden',
actionAccept: 'Akzeptieren',
actionSave: 'Speichern',
- actionCancel: 'Stornieren',
+ actionCancel: 'Abbrechen',
actionDisconnect: 'Trennen',
actionIgnore: 'Ignorieren',
actionDelete: 'Löschen',
- actionBlock: 'Verstecken',
- actionReport: 'Bericht',
+ actionBlock: 'Blockieren',
+ actionReport: 'Melden',
// contact list page
add: 'Hinzufügen',
contactFilter: 'Kontakte',
serverFilter: 'Server',
usernameFilter: 'Benutzername',
- viewProfile: 'Profil Anzeigen',
- messageContact: 'Nachricht Senden',
+ viewProfile: 'Profil anzeigen',
+ messageContact: 'Nachricht senden',
callContact: 'Kontakt Anrufen',
noContacts: 'Keine Kontakte Gefunden',
@@ -762,8 +762,8 @@ const Strings = [
contacts: 'Kontakte',
topics: 'Themen',
subject: 'Titel (optional)',
- create: 'Erstellen',
- sealed: 'Gesichert',
+ create: 'Starten',
+ sealed: 'versiegeln',
newTopic: 'Neues Thema',
new: 'Neu',
@@ -776,9 +776,9 @@ const Strings = [
editSubject: 'Titel bearbeiten',
topicMembers: 'Themenmitglieder',
leaveTopic: 'Verlasse das Thema',
- deleteTopic: 'Das Thema Löschen',
- blockTopic: 'Blockiere das Thema',
- reportTopic: 'Das Thema Melden',
+ deleteTopic: 'Das Thema löschen',
+ blockTopic: 'Das Thema blockieren',
+ reportTopic: 'Das Thema melden',
unknown: 'Unbekannt',
accounts: 'Konten',
@@ -789,8 +789,8 @@ const Strings = [
federatedHost: 'Verbundname',
storageLimit: 'Raum (GB) / Konto',
keyType: 'Schlüsselart',
- enableImage: 'Bilddateien Aktivieren',
- enableAudio: 'Audiodateien Aktivieren',
+ enableImage: 'Bilddateien aktivieren',
+ enableAudio: 'Audiodateien aktivieren',
enableVideo: 'Videodateien aktivieren',
enableBinary: 'Binärdateien aktivieren',
enableCalls: 'Anrufe Ermöglichen',
@@ -823,14 +823,14 @@ const Strings = [
editMessage: 'Nachrichtentext Bearbeiten',
emptyTopic: 'Keine Nachrichten',
- notes: 'Anmerkungen',
+ notes: 'Notizen',
noTopics: 'Keine Themen',
welcome: 'Willkommen bei Databag',
communication: 'Kommunikation für das dezentrale Internet',
setup: 'Richten Sie Ihr Profil ein',
connect: 'Verbinde dich mit Menschen',
- start: 'Eine Konversation Beginnen',
+ start: 'Eine Konversation beginnen',
started: 'Loslegen',
deleteMessage: 'Nachricht Löschen',
@@ -841,7 +841,7 @@ const Strings = [
selectTopic: 'Wählen Sie ein Thema zum Teilen aus',
mfaTitle: 'Zwei-Faktor-Authentifizierung',
- mfaSteps: 'Speichern Sie das SHA256-Geheimnis und bestätigen Sie den Bestätigungscode',
+ mfaSteps: 'Speichern Sie das Geheimnis und bestätigen Sie den Bestätigungscode',
mfaEnter: 'Geben Sie Ihren Bestätigungs-Code ein',
mfaError: 'Verifizierungscodefehler',
mfaDisabled: 'Verifizierung vorübergehend deaktiviert',
@@ -1038,7 +1038,7 @@ const Strings = [
selectTopic: 'Escolha o tópico para compartilhar',
mfaTitle: 'Autenticação de Dois Fatores',
- mfaSteps: 'Salve o segredo SHA256 e confirme o código de verificação',
+ mfaSteps: 'Salve o segredo e confirme o código de verificação',
mfaEnter: 'Digite seu código de verificação',
mfaError: 'erro de código de verificação',
mfaDisabled: 'verificação temporariamente desativada',
@@ -1233,7 +1233,7 @@ const Strings = [
selectTopic: 'Выберите тему для обмена',
mfaTitle: 'Двухфакторная аутентификация',
- mfaSteps: 'Сохраните секрет SHA256 и подтвердите код подтверждения',
+ mfaSteps: 'Сохраните секрет и подтвердите код подтверждения',
mfaEnter: 'Введите Ваш верификационный код',
mfaError: 'ошибка проверочного кода',
mfaDisabled: 'проверка временно отключена',
@@ -1242,7 +1242,222 @@ const Strings = [
disable: 'Отключить',
confirmDisable: 'Отключение двухфакторной аутентификации',
disablePrompt: 'Вы уверены, что хотите отключить двухфакторную аутентификацию?',
- }
+ },
+ {
+ // settings screen
+ languageCode: 'el',
+ visibleRegistry: 'Ορατός Λογαριασμός στο Μητρώο του Διακομιστή',
+ edit: 'Επεξεργασία',
+ enableNotifications: 'Ειδοποιήσεις',
+ allowUnsealed: 'Επιτρέψτε μη σφραγισμένες συζητήσεις',
+ sealedTopics: 'Σφραγισμένες Συζητήσεις',
+ colorMode: 'Λειτουργία Χρώματος',
+ hourMode: 'Ώρα',
+ dateMode: 'Ημερομηνία',
+ language: 'Γλώσσα',
+ logout: 'Αποσύνδεση',
+ changeLogin: 'Αλλαγή Στοιχείων Σύνδεσης',
+ deleteAccount: 'Διαγραφή Λογαριασμού',
+ contacts: 'Επαφές',
+ topics: 'Συζητήσεις',
+ messages: 'Μηνύματα',
+ support: 'Υποστήριξη',
+ blocked: 'Μπλοκαρισμένα',
+ account: 'Λογαριασμός',
+ display: 'Μορφοποίηση',
+ messaging: 'Μηνύματα',
+ timeFull: '24ωρο',
+ timeHalf: '12ωρο',
+ monthStart: 'μμ/ηη',
+ monthEnd: 'ηη/μμ',
+ error: 'Σφάλμα',
+ tryAgain: 'Προσπαθήστε ξανά.',
+
+ // seal wizard
+ sealUnset: 'Δημιουργήστε ένα κλειδί για να ενεργοποιήσετε την κρυπτογράφηση από άκρη σε άκρη (E2EE).',
+ sealUnlocked: 'Απενεργοποιώντας το κλειδί σφράγισης, δεν θα έχετε πρόσβαση σε κρυπτογραφημένες συζητήσεις σε αυτήν τη συσκευή μέχρι να το ενεργοποιήσετε ξανά.',
+ sealLocked: 'Ξεκλειδώστε το κλειδί σφράγισης για να υποστηρίξετε κρυπτογραφημένες συζητήσεις σε αυτήν τη συσκευή.',
+ sealDelete: 'Η διαγραφή του κλειδιού θα αφαιρέσει οριστικά την πρόσβαση σε κρυπτογραφημένες συζητήσεις για ΟΛΕΣ τις συσκευές σας.',
+ password: 'Κωδικός Πρόσβασης',
+ confirmPassword: 'Επιβεβαίωση Κωδικού',
+ generate: 'Δημιουργία',
+ disable: 'Απενεργοποίηση',
+ delete: 'Διαγραφή',
+ unlock: 'Ξεκλείδωμα',
+ removeSeal: 'Αφαίρεση Κλειδιού Σφράγισης',
+ disableSeal: 'Απενεργοποίηση Κλειδιού Σφράγισης',
+ unlockSeal: 'Ξεκλείδωμα Κλειδιού Σφράγισης',
+ typeDelete: 'Πληκτρολογήστε [διαγραφή]',
+ deleteKey: 'διαγραφή',
+ enableTopics: 'Ενεργοποίηση Σφραγισμένων Συζητήσεων',
+ manageTopics: 'Διαχείριση Κλειδιού Σφράγισης',
+ changePassword: 'Αλλαγή κωδικού κλειδιού σφράγισης.',
+ update: 'Αλλαγή',
+ changeKey: 'Αλλαγή Κωδικού Κλειδιού',
+ delayMessage: 'Η δημιουργία κλειδιού μπορεί να διαρκέσει αρκετά λεπτά.',
+ changeMessage: 'Εδώ μπορείτε να αλλάξετε το όνομα χρήστη και/ή τον κωδικό πρόσβασης για το λογαριασμό σας.',
+
+ // settings modals
+ cancel: 'Ακύρωση',
+ confirmLogout: 'Αποσύνδεση',
+ loggingOut: 'Αποσύνδεση',
+ username: 'Όνομα Χρήστη',
+ save: 'Αποθήκευση',
+ notAvailable: 'Το όνομα χρήστη δεν είναι διαθέσιμο',
+ blockedContacts: 'Μπλοκαρισμένες Επαφές',
+ restoreContact: 'Επαναφορά Επαφής;',
+ blockedTopics: 'Μπλοκαρισμένες Συζητήσεις',
+ restoreTopic: 'Επαναφορά Συζήτησης;',
+ blockedMessages: 'Μπλοκαρισμένα Μηνύματα',
+ restoreMessage: 'Επαναφορά Μηνύματος;',
+ close: 'Κλείσιμο',
+ ok: 'Εντάξει',
+ noBlockedContacts: 'Δεν υπάρχουν μπλοκαρισμένες επαφές',
+ noBlockedTopics: 'Δεν υπάρχουν μπλοκαρισμένες συζητήσεις',
+ noBlockedMessages: 'Δεν υπάρχουν μπλοκαρισμένα μηνύματα',
+ restore: 'Επαναφορά',
+
+ // profile page
+ edit: 'Επεξεργασία',
+ name: 'Όνομα',
+ location: 'Τοποθεσία',
+ description: 'Περιγραφή',
+ registryVisible: 'Ορατό στο Μητρώο',
+ editImage: 'Επεξεργασία Εικόνας',
+ editDetails: 'Επεξεργασία Λεπτομερειών',
+
+ // contacts page
+ back: 'Πίσω',
+ deleteContact: 'Διαγραφή Επαφής',
+ confirmDelete: 'Διαγραφή',
+ disconnectContact: 'Αποσύνδεση Επαφής',
+ confirmDisconnect: 'Αποσύνδεση',
+ blockContact: 'Μπλοκάρισμα Επαφής',
+ confirmBlock: 'Μπλοκάρισμα',
+ reportContact: 'Αναφορά Επαφής',
+ confirmReport: 'Αναφορά',
+ confirmed: 'Αποθηκεύτηκε',
+ pending: 'Άγνωστο',
+ connecting: 'Αίτημα Στάλθηκε',
+ connected: 'Συνδέθηκε',
+ requested: 'Λήψη Αιτήματος',
+ unsaved: 'Μη Αποθηκευμένο',
+ offsync: 'Εκτός Συγχρονισμού',
+ actionResync: 'Επανασυγχρονισμός',
+ actionConnect: 'Σύνδεση',
+ actionAccept: 'Αποδοχή',
+ actionSave: 'Αποθήκευση',
+ actionCancel: 'Ακύρωση',
+ actionDisconnect: 'Αποσύνδεση',
+ actionIgnore: 'Παράβλεψη',
+ actionDelete: 'Διαγραφή',
+ actionBlock: 'Μπλοκάρισμα',
+ actionReport: 'Αναφορά',
+ actionLeave: 'Αποχώρηση',
+
+ // contact list page
+ add: 'Προσθήκη',
+ contactFilter: 'Επαφές',
+ serverFilter: 'Διακομιστής',
+ usernameFilter: 'Όνομα Χρήστη',
+ viewProfile: 'Προβολή Προφίλ',
+ messageContact: 'Αποστολή Μηνύματος',
+ callContact: 'Κλήση Επαφής',
+ noContacts: 'Δεν βρέθηκαν Επαφές',
+
+ // channels list
+ profile: 'Προφίλ',
+ contacts: 'Επαφές',
+ topics: 'Συζητήσεις',
+ subject: 'Θέμα (προαιρετικό)',
+ create: 'Δημιουργία',
+ sealed: 'Σφραγισμένο',
+ newTopic: 'Νέο Θέμα',
+ new: 'Νέο',
+
+ // details
+ topic: 'Συζήτηση',
+ host: 'οικοδεσπότης',
+ guest: 'επισκέπτης',
+ leave: 'Αποχώρηση',
+ members: 'Μέλη',
+ editSubject: 'Επεξεργασία Θέματος',
+ topicMembers: 'Μέλη Συζήτησης',
+ leaveTopic: 'Αποχώρηση από τη Συζήτηση',
+ deleteTopic: 'Διαγραφή Συζήτησης',
+ blockTopic: 'Μπλοκάρισμα Συζήτησης',
+ reportTopic: 'Αναφορά Συζήτησης',
+ unknown: 'άγνωστο',
+
+ accounts: 'Λογαριασμοί',
+ createAccount: 'Δημιουργία Λογαριασμού',
+ accessAccount: 'Πρόσβαση Λογαριασμού',
+ token: 'Token',
+ settings: 'Ρυθμίσεις',
+ federatedHost: 'Ομοσπονδιακός Οικοδεσπότης',
+ storageLimit: 'Όριο Αποθήκευσης (GB) / Λογαριασμό',
+ keyType: 'Τύπος Κλειδιού Λογαριασμού',
+ enableImage: 'Ενεργοποίηση Ουράς Εικόνας',
+ enableAudio: 'Ενεργοποίηση Ουράς Ήχου',
+ enableVideo: 'Ενεργοποίηση Ουράς Βίντεο',
+ enableBinary: 'Ενεργοποίηση υπόλοιπων Ψηφιακών Αρχείων',
+ enableCalls: 'Ενεργοποίηση Κλήσεων WebRTC',
+ iceService: 'Υπηρεσία Cloudflare',
+ relayUrl: 'URL Αναμετάδοσης',
+ relayUsername: 'Όνομα Χρήστη Αναμετάδοσης',
+ relayPassword: 'Κωδικός Αναμετάδοσης',
+
+ newMessage: 'Νέο Μήνυμα',
+ fontSize: 'Μέγεθος Γραμματοσειράς',
+ small: 'Μικρό',
+ medium: 'Μεσαίο',
+ large: 'Μεγάλο',
+ fontColor: 'Χρώμα Γραμματοσειράς',
+ selectedColor: 'Επιλεγμένο Χρώμα',
+
+ login: 'Σύνδεση',
+ createAccount: 'Δημιουργία Λογαριασμού',
+ forgotPassword: 'Ξεχάσατε τον Κωδικό;',
+ adminAccess: 'Πρόσβαση Διαχειριστή',
+ server: 'Διακομιστής',
+ access: 'Πρόσβαση',
+ defaultPublic: 'Ο προεπιλεγμένος δημόσιος διακομιστής είναι μόνο για δοκιμές. Χρησιμοποιήστε ιδιωτικό διακομιστή σε διαφορετική περίπτωση.',
+ confirmPassword: 'Επιβεβαίωση Κωδικού',
+ accountLogin: 'Σύνδεση Λογαριασμού',
+ accessAccount: 'Πρόσβαση Λογαριασμού',
+ agree: 'Συμφωνώ με τους Όρους Χρήσης',
+ terms: 'Προβολή Όρων Χρήσης',
+ policy: 'Όροι Χρήσης και Πολιτική Χρήστη',
+
+ editMessage: 'Επεξεργασία Κειμένου Μηνύματος',
+ emptyTopic: 'Κενό Θέμα',
+ noTopics: 'Δεν υπάρχουν Συζητήσεις',
+ notes: 'Σημειώσεις',
+
+ welcome: 'Καλώς Ήρθατε στο Databag',
+ communication: 'Επικοινωνία για τον Αποκεντρωμένο Ιστό',
+ setup: 'Ρυθμίστε το Προφίλ σας',
+ connect: 'Συνδεθείτε με Άτομα',
+ start: 'Ξεκινήστε μια Συνομιλία',
+ started: 'Ξεκινήστε',
+
+ deleteMessage: 'Διαγραφή Μηνύματος',
+ blockMessage: 'Μπλοκάρισμα Μηνύματος',
+ reportMessage: 'Αναφορά Μηνύματος',
+ select: 'Επιλογή',
+ selectTopic: 'Επιλέξτε Συζήτηση για Κοινή Χρήση',
+
+ mfaTitle: 'Επαλήθευση Πολλαπλών Παραγόντων (MFA)',
+ mfaSteps: 'Αποθηκεύστε το μυστικό κωδικό και επιβεβαιώστε τον κωδικό επαλήθευσης',
+ mfaError: 'σφάλμα κωδικού επαλήθευσης',
+ mfaDisabled: 'η επαλήθευση είναι προσωρινά απενεργοποιημένη',
+ mfaConfirm: 'Επιβεβαίωση',
+ mfaEnter: 'Εισαγάγετε τον κωδικό επαλήθευσης σας',
+
+ disable: 'Απενεργοποίηση',
+ confirmDisable: 'Απενεργοποίηση Επαλήθευσης Πολλαπλών Παραγόντων ',
+ disablePrompt: 'Είστε σίγουροι ότι θέλετε να απενεργοποιήσετε την επαλήθευση πολλαπλών παραγόντων;',
+ }
];
export function getLanguageStrings() {
@@ -1268,5 +1483,8 @@ export function getLanguageStrings() {
if (lang === 'ru') {
return Strings[5];
}
+ if (lang === 'el') {
+ return Strings[6];
+ }
return Strings[0];
};
diff --git a/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js
index 41b75334c..fea13e652 100644
--- a/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js
+++ b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js
@@ -279,20 +279,14 @@ export function useTopicItem(item, hosting, remove, contentKey) {
};
const clickableText = (text) => {
- const urlPatternn = new RegExp('^(https?:\\/\\/)?'+ // protocol
- '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
- '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
- '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
- '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
- '(\\#[-a-z\\d_]*)?$','i'); // fragment locator
-
+ const urlPattern = new RegExp('(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)');
const hostPattern = new RegExp('^https?:\\/\\/', 'i');
let clickable = [];
let group = '';
const words = text == null ? [''] : text.split(' ');
words.forEach((word, index) => {
- if (!!urlPatternn.test(word)) {
+ if (!!urlPattern.test(word)) {
clickable.push(
apt-get -y install net-tools
apt-get -y install jq
- apt-get -y install netcat
+ apt-get -y install netcat-openbsd
apt-get -y install unzip
apt-get -y install wget
apt-get -y install git
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 000000000..466ce1e6b
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,37 @@
+name: databag-dev
+
+services:
+ net-web:
+ build:
+ context: ./net/web
+ dockerfile: Dockerfile.dev
+ working_dir: /app
+ volumes:
+ - ./net/web:/app
+ command: sh -c "yarn && chokidar '**/*.js' '**/*.ts' -c 'yarn run build' --debounce 18000 --initial --ignore node_modules --ignore build"
+ net-server:
+ build:
+ context: ./net/server
+ dockerfile: Dockerfile.dev
+ ports:
+ - 127.0.0.1:7000:7000
+ volumes:
+ - ./net/server:/app/databag/net/server
+ - ./dev_database:/var/lib/databag
+ - ./net/transform:/opt/databag/transform
+ - ./net/web/build:/app/databag/net/web/build
+ working_dir: /app
+ environment:
+ - ADMIN=password
+ - DEV=1
+ command: /app/databag/net/server/entrypoint.sh
+ net-repeater:
+ build:
+ context: ./net/repeater
+ dockerfile: Dockerfile.dev
+ working_dir: /app
+ volumes:
+ - ./net/repeater:/app
+ ports:
+ - 127.0.0.1:7878:7878
+ command: go run main.go
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..0184bc942
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,14 @@
+name: databag
+
+services:
+ app:
+ build: .
+ ports:
+ - 127.0.0.1:7000:7000
+ volumes:
+ - database:/var/lib/databag
+ environment:
+ - ADMIN=password
+
+volumes:
+ database:
diff --git a/examples/docker-basic/docker-compose.yml b/examples/docker-basic/docker-compose.yml
new file mode 100644
index 000000000..92dec0cae
--- /dev/null
+++ b/examples/docker-basic/docker-compose.yml
@@ -0,0 +1,14 @@
+name: databag-example-http
+
+services:
+ app:
+ image: balzack/databag:latest
+ restart: unless-stopped
+ ports:
+ - 127.0.0.1:7000:7000
+ volumes:
+ - database:/var/lib/databag
+ environment:
+ - ADMIN=password
+volumes:
+ database:
diff --git a/examples/docker-ssl/Caddyfile b/examples/docker-ssl/Caddyfile
new file mode 100644
index 000000000..0eafd0b13
--- /dev/null
+++ b/examples/docker-ssl/Caddyfile
@@ -0,0 +1,3 @@
+example.com {
+ reverse_proxy http://app:443
+}
diff --git a/examples/docker-ssl/docker-compose.yml b/examples/docker-ssl/docker-compose.yml
new file mode 100644
index 000000000..b13856f15
--- /dev/null
+++ b/examples/docker-ssl/docker-compose.yml
@@ -0,0 +1,29 @@
+name: databag-example-ssl
+
+services:
+ caddy:
+ image: caddy
+ restart: unless-stopped
+ ports:
+ - 80:80
+ - 443:443
+ volumes:
+ # Edit the Caddyfile and replace "example.com" with your own domain
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
+ # Recommended by Caddy
+ - caddy_data:/data
+ - caddy_config:/config
+ depends_on:
+ - app
+ app:
+ image: balzack/databag:latest
+ restart: unless-stopped
+ volumes:
+ - database:/var/lib/databag
+ environment:
+ - ADMIN=password
+ - DATABAG_PORT=443
+volumes:
+ database:
+ caddy_data:
+ caddy_config:
diff --git a/examples/linux/databag.service b/examples/linux/databag.service
new file mode 100644
index 000000000..ca43cf8d1
--- /dev/null
+++ b/examples/linux/databag.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=databag server
+After=network.target
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=databag
+ExecStart=/app/databag/net/server/entrypoint.sh
+
+# [Service]
+# Environment="ADMIN=password"
+
+[Install]
+WantedBy=multi-user.target
diff --git a/examples/linux/install.sh b/examples/linux/install.sh
new file mode 100755
index 000000000..db7d7145b
--- /dev/null
+++ b/examples/linux/install.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+set -e
+
+function confirm() {
+ read -p "Are you sure you want to continue? [Y/n] " reply
+ if [ "$reply" != "Y" ] && [ "$reply" != "y" ]; then
+ echo "Aborting"
+ exit 1
+ fi
+}
+
+if ! id "databag" >/dev/null 2>&1; then
+ echo "User databag not found, creating..."
+ confirm
+ sudo useradd databag
+fi
+
+if [[ ! -d "/app/databag" ]]; then
+ echo "Creating app directory for databag, this requires sudo permissions"
+ sudo rm -rf /app/databag || true
+ sudo mkdir -p /app/databag
+
+ echo "Downloading databag repository into /app/databag"
+ git clone --depth 1 https://github.com/balzack/databag.git /app/databag
+ sudo chown -R databag:databag /app/databag
+fi
+cd /app/databag
+
+# You might be running this script from the root of this repository
+if [[ "/app/databag" != $(pwd) ]]; then
+ cd ../..
+fi
+
+if [[ "/app/databag" != $(pwd) ]]; then
+ echo "Install databag must be done from /app/databag"
+ echo "Please re-clone the github repository into /app/databag, like so:"
+ echo "mkdir -p /app; https://github.com/balzack/databag /app/databag"
+ exit 1
+fi
+
+if [[ -z $(which yarn) ]]; then
+ echo "Yarn is not installed, installing..."
+ npm install --global yarn
+fi
+
+echo "Building frontend files..."
+cd net/web
+yarn --frozen-lockfile
+echo "Removing old frontend files, requires sudo permissions..."
+sudo rm -rf build
+yarn run build
+sudo chown -R databag:databag build
+cd ../..
+
+echo "Building backend files..."
+cd net/server
+CGO_ENABLED=1 go build -o databag .
+cd ../..
+
+echo "Creating databag locations..."
+sudo mkdir -p /opt/databag
+sudo mkdir -p /var/lib/databag
+sudo chown -R databag:databag /var/lib/databag
+
+echo "Copying transform scripts..."
+sudo mkdir -p /opt/databag/transform
+sudo cp net/transform/*.sh /opt/databag/transform/
+sudo chmod +x /opt/databag/transform/*.sh
+
+if [[ ! -f /etc/systemd/system/databag.service ]]; then
+ function createService() {
+ echo "Creating databag service, requires sudo permissions..."
+ sudo cp examples/linux/databag.service /etc/systemd/system/databag.service
+ sudo chmod 664 /etc/systemd/system/databag.service
+ sudo systemctl daemon-reload
+ }
+ function startService() {
+ echo "Starting databag service..."
+ sudo systemctl start databag.service
+ }
+ createService $? "Failed to install databag service"
+ startService $? "Failed to start databag service"
+fi
+echo ""
+echo "Done"
diff --git a/examples/linux/uninstall.sh b/examples/linux/uninstall.sh
new file mode 100755
index 000000000..b31075f8f
--- /dev/null
+++ b/examples/linux/uninstall.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+function confirm() {
+ read -p "Are you sure you want to continue? [Y/n] " reply
+ if [ "$reply" != "Y" ] && [ "$reply" != "y" ]; then
+ echo "Aborting"
+ exit 1
+ fi
+}
+
+echo "Stopping, disabling and removing databag service..."
+confirm
+sudo systemctl stop databag.service
+sudo systemctl disable databag.service
+sudo rm /etc/systemd/system/databag.service
+sudo systemctl reload
+
+echo "Removing databag data..."
+confirm
+sudo rm -rf /app/databag /opt/databag /var/lib/databag
+if [ -z "$(ls -A /app)" ]; then
+ sudo rmdir /app
+fi
+
+echo "Removing databag user..."
+confirm
+sudo userdel databag
+
+echo "Done"
diff --git a/net/container/Dockerfile b/net/container/Dockerfile
deleted file mode 100644
index 83e391c0a..000000000
--- a/net/container/Dockerfile
+++ /dev/null
@@ -1,67 +0,0 @@
-FROM ubuntu:20.04 as build
-ARG TARGETPLATFORM
-LABEL maintainer="roland.osborne@gmail.com"
-
-EXPOSE 7000
-
-ENV TZ=America/Los_Angeles
-RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
-
-RUN apt-get update
-
-RUN apt-get -y install curl net-tools jq netcat unzip wget git vim fail2ban imagemagick-6.q16 ffmpeg build-essential sqlite3 npm
-
-RUN apt-get -y upgrade
-
-RUN npm install --global yarn
-RUN npm install -g n
-RUN n stable
-
-RUN mkdir /app
-
-RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then ARCHITECTURE=amd64; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCHITECTURE=arm64; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCHITECTURE=aarch64; else ARCHITECTURE=unsupported; fi \
- && wget -P /app https://go.dev/dl/go1.22.2.linux-${ARCHITECTURE}.tar.gz \
- && tar -C /usr/local -xzf /app/go1.22.2.linux-${ARCHITECTURE}.tar.gz
-
-RUN git clone https://github.com/balzack/databag.git /app/databag
-
-RUN yarn config set network-timeout 300000
-RUN yarn --cwd /app/databag/net/web install
-RUN yarn --cwd /app/databag/net/web build
-RUN cd /app/databag/net/server; /usr/local/go/bin/go build databag
-
-RUN mkdir /opt/databag
-
-ADD transform /opt/databag/transform
-
-RUN mkdir -p /var/lib/databag
-
-RUN echo 'export PATH=$PATH:/usr/local/go/bin' >> /root/.bashrc
-RUN echo "set expandtab\nset tabstop=2\nset softtabstop=2\nset shiftwidth=2\nset encoding=utf-8\nset fileencoding=utf-8\n" > /root/.vimrc
-RUN echo "bind 'set mark-symlinked-directories on'" >> /root/.bashrc
-
-ADD entrypoint.sh /app
-ADD dev_setup.sh /app
-
-RUN rm -rf /usr/local/go
-RUN rm -rf /root/go
-RUN rm -rf /app/go*
-RUN rm -rf /root/.cache/go*
-
-RUN yarn cache clean
-RUN rm -rf /app/databag/app
-RUN rm -rf /app/databag/net/web/node_modules
-
-RUN n prune
-RUN npm uninstall -g n
-RUN rm -rf /usr/local/n
-RUN rm -rf /usr/local/bin/node
-
-RUN apt-get -y remove git build-essential npm vim nodejs linux-libc-dev
-RUN rm -rf /var/lib/apt/lists
-
-FROM scratch
-COPY --from=build / /
-
-ENTRYPOINT ["/app/entrypoint.sh"]
-
diff --git a/net/container/Dockerfile.dev b/net/container/Dockerfile.dev
deleted file mode 100644
index 5030c6ba8..000000000
--- a/net/container/Dockerfile.dev
+++ /dev/null
@@ -1,50 +0,0 @@
-FROM ubuntu:20.04 as build
-ARG TARGETPLATFORM
-LABEL maintainer="roland.osborne@gmail.com"
-
-EXPOSE 7000
-
-ENV TZ=America/Los_Angeles
-RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
-
-RUN apt-get update
-
-RUN apt-get -y install curl net-tools jq netcat unzip wget git vim fail2ban imagemagick-6.q16 ffmpeg build-essential sqlite3 npm
-
-RUN apt-get -y upgrade
-
-RUN npm install --global yarn
-RUN npm install -g n
-RUN n stable
-
-RUN mkdir /app
-
-RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then ARCHITECTURE=amd64; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCHITECTURE=arm64; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCHITECTURE=aarch64; else ARCHITECTURE=unsupported; fi \
- && wget -P /app https://go.dev/dl/go1.22.2.linux-${ARCHITECTURE}.tar.gz \
- && tar -C /usr/local -xzf /app/go1.22.2.linux-${ARCHITECTURE}.tar.gz
-
-RUN git clone https://github.com/balzack/databag.git /app/databag
-
-RUN yarn config set network-timeout 300000
-RUN yarn --cwd /app/databag/net/web install
-RUN yarn --cwd /app/databag/net/web build
-RUN cd /app/databag/net/server; /usr/local/go/bin/go build databag
-
-RUN mkdir /opt/databag
-
-ADD transform /opt/databag/transform
-
-RUN mkdir -p /var/lib/databag
-
-RUN echo 'export PATH=$PATH:/usr/local/go/bin' >> /root/.bashrc
-RUN echo "set expandtab\nset tabstop=2\nset softtabstop=2\nset shiftwidth=2\nset encoding=utf-8\nset fileencoding=utf-8\n" > /root/.vimrc
-RUN echo "bind 'set mark-symlinked-directories on'" >> /root/.bashrc
-
-ADD dev_setup.sh /app
-ADD entrypoint.sh /app
-
-FROM scratch
-COPY --from=build / /
-
-ENTRYPOINT ["/app/entrypoint.sh"]
-
diff --git a/net/container/dev_setup.sh b/net/container/dev_setup.sh
deleted file mode 100755
index 8a026811f..000000000
--- a/net/container/dev_setup.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-cd /root
-wget -P /app https://go.dev/dl/go1.22.2.linux-amd64.tar.gz
-tar -C /usr/local -xzf /app/go1.22.2.linux-amd64.tar.gz
-
-apt-get update
-apt-get -y install git build-essential npm vim
-
-curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
-apt install -y nodejs
-
-npm install --global yarn
-npm install -g n
-n stable
-
-cd /app/databag
-git checkout .
-
-yarn --cwd /app/databag/net/web install
-yarn --cwd /app/databag/net/web build
-cd /app/databag/net/server; /usr/local/go/bin/go build databag
diff --git a/net/container/docker-compose.dev.yml b/net/container/docker-compose.dev.yml
deleted file mode 100644
index ef69a0340..000000000
--- a/net/container/docker-compose.dev.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-version: "3.9"
-services:
- databag:
- environment:
- - DEV=1
- container_name: databag
- image: balzack/databag:latest
- ports:
- - "7000:7000"
- volumes:
- - ./databag-data:/var/lib/databag
-
diff --git a/net/container/docker-compose.yml b/net/container/docker-compose.yml
deleted file mode 100644
index b0878d245..000000000
--- a/net/container/docker-compose.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-version: "3.9"
-services:
- databag:
- container_name: databag
- image: balzack/databag:latest
- ports:
- - "7000:7000"
- volumes:
- - ./databag-data:/var/lib/databag
-
diff --git a/net/container/entrypoint.sh b/net/container/entrypoint.sh
deleted file mode 100755
index 4b3ec4e57..000000000
--- a/net/container/entrypoint.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-set -e
-
-sqlite3 /var/lib/databag/databag.db "VACUUM;"
-sqlite3 /var/lib/databag/databag.db "CREATE TABLE IF NOT EXISTS 'configs' ('id' integer NOT NULL UNIQUE,'config_id' text NOT NULL,'str_value' text,'num_value' integer,'bool_value' numeric,'bin_value' blob,PRIMARY KEY ('id'));"
-sqlite3 /var/lib/databag/databag.db "CREATE UNIQUE INDEX IF NOT EXISTS 'idx_configs_config_id' ON 'configs'('config_id');"
-
-if [[ -v ADMIN ]]; then
- sqlite3 /var/lib/databag/databag.db "delete from configs where config_id='configured';"
- sqlite3 /var/lib/databag/databag.db "delete from configs where config_id='token';"
- sqlite3 /var/lib/databag/databag.db "insert into configs (config_id, str_value) values ('token', '$ADMIN');"
- sqlite3 /var/lib/databag/databag.db "insert into configs (config_id, bool_value) values ('configured', true);"
-fi
-
-if [ "$DEV" == "1" ]; then
- cd /app/databag/net/server
- ./databag -p 7000 -w /app/databag/net/web/build -s /var/lib/databag -t /opt/databag/transform &
- /app/dev_setup.sh || true
- while true; do
- sleep 1;
- done
-else
- cd /app/databag/net/server
- ./databag -p 7000 -w /app/databag/net/web/build -s /var/lib/databag -t /opt/databag/transform
-fi
diff --git a/net/repeater/Dockerfile b/net/repeater/Dockerfile
new file mode 100644
index 000000000..29f8c0b7b
--- /dev/null
+++ b/net/repeater/Dockerfile
@@ -0,0 +1,9 @@
+FROM golang:alpine
+
+WORKDIR /app
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN go build -o repeater .
+EXPOSE 7878
+ENTRYPOINT ./repeater
diff --git a/net/repeater/Dockerfile.dev b/net/repeater/Dockerfile.dev
new file mode 100644
index 000000000..3865d5480
--- /dev/null
+++ b/net/repeater/Dockerfile.dev
@@ -0,0 +1,6 @@
+FROM golang:alpine
+
+WORKDIR /app
+COPY go.mod go.sum ./
+RUN go mod download
+RUN rm go.mod go.sum
diff --git a/net/repeater/go.mod b/net/repeater/go.mod
new file mode 100644
index 000000000..dc1a8e5fd
--- /dev/null
+++ b/net/repeater/go.mod
@@ -0,0 +1,70 @@
+module repeater
+
+go 1.17
+
+require (
+ github.com/google/uuid v1.6.0
+ github.com/gorilla/handlers v1.5.2
+ github.com/gorilla/mux v1.8.1
+ github.com/gorilla/websocket v1.5.1
+ github.com/kr/pretty v0.3.1
+ github.com/stretchr/testify v1.8.4
+ github.com/theckman/go-securerandom v0.1.1
+ github.com/valyala/fastjson v1.6.4
+ golang.org/x/crypto v0.24.0
+ gorm.io/driver/sqlite v1.5.5
+ gorm.io/gorm v1.25.9
+)
+
+require (
+ cloud.google.com/go v0.112.1 // indirect
+ cloud.google.com/go/compute v1.24.0 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/firestore v1.15.0 // indirect
+ cloud.google.com/go/iam v1.1.7 // indirect
+ cloud.google.com/go/longrunning v0.5.5 // indirect
+ cloud.google.com/go/storage v1.40.0 // indirect
+ firebase.google.com/go/v4 v4.14.1 // indirect
+ github.com/MicahParks/keyfunc v1.9.0 // indirect
+ github.com/SherClockHolmes/webpush-go v1.3.0 // indirect
+ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/s2a-go v0.1.7 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+ github.com/googleapis/gax-go/v2 v2.12.3 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/pquerna/otp v1.4.0 // indirect
+ github.com/rogpeppe/go-internal v1.12.0 // indirect
+ go.opencensus.io v0.24.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ go.opentelemetry.io/otel v1.24.0 // indirect
+ go.opentelemetry.io/otel/metric v1.24.0 // indirect
+ go.opentelemetry.io/otel/trace v1.24.0 // indirect
+ golang.org/x/net v0.23.0 // indirect
+ golang.org/x/oauth2 v0.18.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
+ golang.org/x/sys v0.21.0 // indirect
+ golang.org/x/text v0.16.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+ google.golang.org/api v0.170.0 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/appengine/v2 v2.0.2 // indirect
+ google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect
+ google.golang.org/grpc v1.62.1 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/net/repeater/go.sum b/net/repeater/go.sum
new file mode 100644
index 000000000..0277da3a8
--- /dev/null
+++ b/net/repeater/go.sum
@@ -0,0 +1,273 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
+cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
+cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
+cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
+cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
+cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
+cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
+cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
+cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
+cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=
+cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
+firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
+firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
+github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
+github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
+github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
+github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
+github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
+github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
+github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/theckman/go-securerandom v0.1.1 h1:5KctSyM0D5KKFK+bsypIyLq7yik0CEaI5i2fGcUGcsQ=
+github.com/theckman/go-securerandom v0.1.1/go.mod h1:bmkysLfBH6i891sBpcP4xRM3XIB7jMeiKJB31jlResI=
+github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
+github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
+google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
+google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
+google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc=
+google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
+gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
+gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
+gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/net/repeater/internal/api_notify.go b/net/repeater/internal/api_notify.go
new file mode 100644
index 000000000..ff1f0f671
--- /dev/null
+++ b/net/repeater/internal/api_notify.go
@@ -0,0 +1,61 @@
+package repeater
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "os"
+ "runtime"
+ "strings"
+ "fmt"
+ "firebase.google.com/go/v4/messaging"
+)
+
+func ParseRequest(r *http.Request, w http.ResponseWriter, obj interface{}) error {
+ r.Body = http.MaxBytesReader(w, r.Body, 1024)
+ dec := json.NewDecoder(r.Body)
+ return dec.Decode(&obj)
+}
+
+func WriteResponse(w http.ResponseWriter, v interface{}) {
+ body, err := json.Marshal(v)
+ if err != nil {
+ _, file, line, _ := runtime.Caller(1)
+ p, _ := os.Getwd()
+ log.Printf("%s:%d %s", strings.TrimPrefix(file, p), line, err.Error())
+ w.WriteHeader(http.StatusInternalServerError)
+ } else {
+ w.Header().Set("Content-Type", "application/json")
+ w.Write(body)
+ }
+}
+
+//Notify proxies push notification to device
+func Notify(w http.ResponseWriter, r *http.Request) {
+
+ var msg PushMessage
+ if err := ParseRequest(r, w, &msg); err != nil {
+ ErrResponse(w, http.StatusBadRequest, err)
+ return
+ }
+
+ // See documentation on defining a message payload.
+ notification := &messaging.Notification{ Title: msg.Title, Body: msg.Body }
+ message := &messaging.Message{
+ Notification : notification,
+ Token: msg.Token,
+ }
+
+ // Send a message to the device corresponding to the provided
+ // registration token.
+ response, err := FCMClient.Send(FCMContext, message)
+ if err != nil {
+ ErrResponse(w, http.StatusBadRequest, err)
+ return
+ }
+ // Response is a message ID string.
+ fmt.Println("Successfully sent message:", response)
+
+ res := &PushResponse{ Message: response }
+ WriteResponse(w, res);
+}
diff --git a/net/repeater/internal/logger.go b/net/repeater/internal/logger.go
new file mode 100644
index 000000000..4d59dded1
--- /dev/null
+++ b/net/repeater/internal/logger.go
@@ -0,0 +1,60 @@
+package repeater
+
+import (
+ "github.com/kr/pretty"
+ "log"
+ "net/http"
+ "os"
+ "runtime"
+ "strings"
+ "time"
+)
+
+//Logger prints endpoint details
+func Logger(inner http.Handler, name string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+
+ inner.ServeHTTP(w, r)
+
+ log.Printf(
+ "%s %s %s %s",
+ r.Method,
+ r.RequestURI,
+ name,
+ time.Since(start),
+ )
+ })
+}
+
+//ErrResponse prints detailed error event and sets response
+func ErrResponse(w http.ResponseWriter, code int, err error) {
+ if err != nil {
+ _, file, line, _ := runtime.Caller(1)
+ p, _ := os.Getwd()
+ log.Printf("%s:%d %s", strings.TrimPrefix(file, p), line, err.Error())
+ }
+ w.WriteHeader(code)
+}
+
+//ErrMsg prints detailed error event
+func ErrMsg(err error) {
+ if err != nil {
+ _, file, line, _ := runtime.Caller(1)
+ p, _ := os.Getwd()
+ log.Printf("%s:%d %s", strings.TrimPrefix(file, p), line, err.Error())
+ }
+}
+
+//LogMsg prints detailed error string
+func LogMsg(msg string) {
+ _, file, line, _ := runtime.Caller(1)
+ p, _ := os.Getwd()
+ log.Printf("%s:%d %s", strings.TrimPrefix(file, p), line, msg)
+}
+
+//PrintMsg prints debug message
+func PrintMsg(obj interface{}) {
+ pretty.Println(obj)
+}
+
diff --git a/net/repeater/internal/models.go b/net/repeater/internal/models.go
new file mode 100644
index 000000000..0dc613be3
--- /dev/null
+++ b/net/repeater/internal/models.go
@@ -0,0 +1,14 @@
+package repeater
+
+//PushMessage notification
+type PushMessage struct {
+ Title string `json:"title"`
+ Body string `json:"body"`
+ Token string `json:"token"`
+}
+
+//PushResponse notification response
+type PushResponse struct {
+ Message string `json:"message"`
+}
+
diff --git a/net/repeater/internal/routers.go b/net/repeater/internal/routers.go
new file mode 100644
index 000000000..50b422f4d
--- /dev/null
+++ b/net/repeater/internal/routers.go
@@ -0,0 +1,66 @@
+package repeater
+
+import (
+ "firebase.google.com/go/v4/messaging"
+ firebase "firebase.google.com/go/v4"
+ "github.com/gorilla/mux"
+ "net/http"
+ "strings"
+ "context"
+ "log"
+)
+
+type route struct {
+ Name string
+ Method string
+ Pattern string
+ HandlerFunc http.HandlerFunc
+}
+
+type routes []route
+
+var FCMClient *messaging.Client
+var FCMContext context.Context
+
+//NewRouter allocate router for databag API
+func NewRouter() *mux.Router {
+
+ app, err := firebase.NewApp(context.Background(), nil)
+ if err != nil {
+ log.Fatalf("error initializing app: %v\n", err)
+ }
+
+ ctx := context.Background()
+ client, err := app.Messaging(ctx)
+ if err != nil {
+ log.Fatalf("error getting Messaging client: %v\n", err)
+ }
+ FCMClient = client
+ FCMContext = ctx
+
+ router := mux.NewRouter().StrictSlash(true)
+ for _, route := range endpoints {
+ var handler http.Handler
+ handler = route.HandlerFunc
+ handler = Logger(handler, route.Name)
+
+ router.
+ Methods(route.Method).
+ Path(route.Pattern).
+ Name(route.Name).
+ Handler(handler)
+ }
+
+ return router
+}
+
+var endpoints = routes{
+
+ route{
+ "Notify",
+ strings.ToUpper("Post"),
+ "/notify",
+ Notify,
+ },
+}
+
diff --git a/net/repeater/main.go b/net/repeater/main.go
new file mode 100644
index 000000000..bb6ac74e6
--- /dev/null
+++ b/net/repeater/main.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ app "repeater/internal"
+ "github.com/gorilla/handlers"
+ "log"
+ "net/http"
+ "os"
+)
+
+func main() {
+ var cert string
+ var key string
+
+ port := ":7878"
+
+ args := os.Args[1:];
+ for i := 0; i + 1 < len(args); i += 2 {
+ if args[i] == "-p" {
+ port = ":" + args[i + 1]
+ } else if args[i] == "-c" {
+ cert = args[i + 1]
+ } else if args[i] == "-k" {
+ key = args[i + 1]
+ }
+ }
+
+ router := app.NewRouter()
+ origins := handlers.AllowedOrigins([]string{"*"})
+ headers := handlers.AllowedHeaders([]string{"content-type", "authorization", "credentials"})
+ methods := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"})
+
+ if cert != "" && key != "" {
+ log.Printf("using args:" + " -p " + port[1:] + " -c " + cert + " -k " + key)
+ log.Fatal(http.ListenAndServeTLS(port, cert, key, handlers.CORS(origins, headers, methods)(router)))
+ } else {
+ log.Printf("using args:" + " -p " + port[1:])
+ log.Fatal(http.ListenAndServe(port, handlers.CORS(origins, headers, methods)(router)))
+ }
+}
diff --git a/net/repeater/repeater.service b/net/repeater/repeater.service
new file mode 100644
index 000000000..84c389420
--- /dev/null
+++ b/net/repeater/repeater.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=push notification repeater for databag network
+After=network.target
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=root
+ExecStart=/usr/bin/repeater -p 443 -c /etc/letsencrypt/live/repeater.coredb.org/fullchain.pem -k /etc/letsencrypt/live/repeater.coredb.org/privkey.pem
+
+[Service]
+Environment="GOOGLE_APPLICATION_CREDENTIALS=/opt/databag/databag.json"
+
+[Install]
+WantedBy=multi-user.target
diff --git a/net/server/.dockerignore b/net/server/.dockerignore
new file mode 100644
index 000000000..99b97449c
--- /dev/null
+++ b/net/server/.dockerignore
@@ -0,0 +1 @@
+databag
diff --git a/net/server/.gitignore b/net/server/.gitignore
new file mode 100644
index 000000000..e6105547f
--- /dev/null
+++ b/net/server/.gitignore
@@ -0,0 +1 @@
+/databag
\ No newline at end of file
diff --git a/net/server/Dockerfile.dev b/net/server/Dockerfile.dev
new file mode 100644
index 000000000..4cc17e124
--- /dev/null
+++ b/net/server/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM golang:alpine
+
+RUN apk add build-base imagemagick sqlite ffmpeg curl
+
+RUN mkdir -p /opt/databag
+RUN mkdir -p /var/lib/databag
+RUN mkdir -p /app/databag/net
+
+RUN mkdir -p /tmp/databag-go-cache
+WORKDIR /tmp/databag-go-cache
+COPY go.mod go.sum ./
+RUN go mod download
+
+WORKDIR /
+RUN rm -rf /tmp/databag-go-cache
diff --git a/net/server/entrypoint.sh b/net/server/entrypoint.sh
new file mode 100755
index 000000000..63c173f56
--- /dev/null
+++ b/net/server/entrypoint.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+set -e
+
+sqlite3 /var/lib/databag/databag.db "VACUUM;"
+sqlite3 /var/lib/databag/databag.db "CREATE TABLE IF NOT EXISTS 'configs' ('id' integer NOT NULL UNIQUE,'config_id' text NOT NULL,'str_value' text,'num_value' integer,'bool_value' numeric,'bin_value' blob,PRIMARY KEY ('id'));"
+sqlite3 /var/lib/databag/databag.db "CREATE UNIQUE INDEX IF NOT EXISTS 'idx_configs_config_id' ON 'configs'('config_id');"
+
+if [[ -n "$ADMIN" ]]; then
+ sqlite3 /var/lib/databag/databag.db "delete from configs where config_id='configured';"
+ sqlite3 /var/lib/databag/databag.db "delete from configs where config_id='token';"
+ sqlite3 /var/lib/databag/databag.db "insert into configs (config_id, str_value) values ('token', '$ADMIN');"
+ sqlite3 /var/lib/databag/databag.db "insert into configs (config_id, bool_value) values ('configured', true);"
+fi
+
+if [[ -z "$DATABAG_PORT" ]]; then
+ DATABAG_PORT=7000
+fi
+
+cd /app/databag/net/server
+if [[ "$DEV" == "1" ]]; then
+ CGO_ENABLED=1 go run main.go -p $DATABAG_PORT -w /app/databag/net/web/build -s /var/lib/databag -t /opt/databag/transform
+else
+ ./databag -p $DATABAG_PORT -w /app/databag/net/web/build -s /var/lib/databag -t /opt/databag/transform
+fi
diff --git a/net/server/internal/api_addAccountApp.go b/net/server/internal/api_addAccountApp.go
index e2b020937..43c5fbc32 100644
--- a/net/server/internal/api_addAccountApp.go
+++ b/net/server/internal/api_addAccountApp.go
@@ -34,7 +34,11 @@ func AddAccountApp(w http.ResponseWriter, r *http.Request) {
return;
}
- opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
+ algorithm := otp.AlgorithmSHA256;
+ if account.MFAAlgorithm == APPMFASHA1 {
+ algorithm = otp.AlgorithmSHA1
+ }
+ opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: algorithm}
if valid, _ := totp.ValidateCustom(code, account.MFASecret, time.Now(), opts); !valid {
err := store.DB.Transaction(func(tx *gorm.DB) error {
if account.MFAFailedTime + APPMFAFailPeriod > curTime {
diff --git a/net/server/internal/api_addAdminMFAuth.go b/net/server/internal/api_addAdminMFAuth.go
index c7bd97c69..aef5d98c5 100644
--- a/net/server/internal/api_addAdminMFAuth.go
+++ b/net/server/internal/api_addAdminMFAuth.go
@@ -25,7 +25,7 @@ func AddAdminMFAuth(w http.ResponseWriter, r *http.Request) {
Issuer: APPMFAIssuer,
AccountName: "admin",
Digits: otp.DigitsSix,
- Algorithm: otp.AlgorithmSHA256,
+ Algorithm: otp.AlgorithmSHA1,
})
err = store.DB.Transaction(func(tx *gorm.DB) error {
diff --git a/net/server/internal/api_addMultiFactorAuth.go b/net/server/internal/api_addMultiFactorAuth.go
index 323ccb9ef..ba135c3f3 100644
--- a/net/server/internal/api_addMultiFactorAuth.go
+++ b/net/server/internal/api_addMultiFactorAuth.go
@@ -24,7 +24,7 @@ func AddMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
Issuer: APPMFAIssuer,
AccountName: account.Handle,
Digits: otp.DigitsSix,
- Algorithm: otp.AlgorithmSHA256,
+ Algorithm: otp.AlgorithmSHA1,
})
err = store.DB.Transaction(func(tx *gorm.DB) error {
diff --git a/net/server/internal/api_setAdminAccess.go b/net/server/internal/api_setAdminAccess.go
index 30426a4f8..f25db66a3 100644
--- a/net/server/internal/api_setAdminAccess.go
+++ b/net/server/internal/api_setAdminAccess.go
@@ -40,8 +40,13 @@ func SetAdminAccess(w http.ResponseWriter, r *http.Request) {
return;
}
- secret := getStrConfigValue(CNFMFASecret, "");
- opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
+ secret := getStrConfigValue(CNFMFASecret, "")
+ algorithm := getStrConfigValue(CNFMFAAlgorithm, APPMFASHA256)
+ mfaAlgorithm := otp.AlgorithmSHA256
+ if algorithm == APPMFASHA1 {
+ mfaAlgorithm = otp.AlgorithmSHA1
+ }
+ opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: mfaAlgorithm}
if valid, _ := totp.ValidateCustom(code, secret, time.Now(), opts); !valid {
err := store.DB.Transaction(func(tx *gorm.DB) error {
if failedTime + APPMFAFailPeriod > curTime {
diff --git a/net/server/internal/api_setAdminMFAuth.go b/net/server/internal/api_setAdminMFAuth.go
index 66ebd516a..016758e25 100644
--- a/net/server/internal/api_setAdminMFAuth.go
+++ b/net/server/internal/api_setAdminMFAuth.go
@@ -38,39 +38,44 @@ func SetAdminMFAuth(w http.ResponseWriter, r *http.Request) {
return;
}
+ mfaAlgorithm := APPMFASHA1
secret := getStrConfigValue(CNFMFASecret, "");
- opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
+ opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1}
if valid, _ := totp.ValidateCustom(code, secret, time.Now(), opts); !valid {
- err := store.DB.Transaction(func(tx *gorm.DB) error {
- if failedTime + APPMFAFailPeriod > curTime {
- if res := tx.Clauses(clause.OnConflict{
- Columns: []clause.Column{{Name: "config_id"}},
- DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
- }).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil {
- return res
- }
- } else {
- if res := tx.Clauses(clause.OnConflict{
- Columns: []clause.Column{{Name: "config_id"}},
- DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
- }).Create(&store.Config{ConfigID: CNFMFAFailedTime, NumValue: curTime}).Error; res != nil {
- return res
- }
- if res := tx.Clauses(clause.OnConflict{
- Columns: []clause.Column{{Name: "config_id"}},
- DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
- }).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: 1}).Error; res != nil {
- return res
+ mfaAlgorithm = APPMFASHA256
+ opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
+ if valid, _ := totp.ValidateCustom(code, secret, time.Now(), opts); !valid {
+ err := store.DB.Transaction(func(tx *gorm.DB) error {
+ if failedTime + APPMFAFailPeriod > curTime {
+ if res := tx.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "config_id"}},
+ DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
+ }).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil {
+ return res
+ }
+ } else {
+ if res := tx.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "config_id"}},
+ DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
+ }).Create(&store.Config{ConfigID: CNFMFAFailedTime, NumValue: curTime}).Error; res != nil {
+ return res
+ }
+ if res := tx.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "config_id"}},
+ DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
+ }).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: 1}).Error; res != nil {
+ return res
+ }
}
+ return nil
+ })
+ if err != nil {
+ LogMsg("failed to increment fail count");
}
- return nil
- })
- if err != nil {
- LogMsg("failed to increment fail count");
- }
- ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
- return
+ ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
+ return
+ }
}
err := store.DB.Transaction(func(tx *gorm.DB) error {
@@ -81,6 +86,13 @@ func SetAdminMFAuth(w http.ResponseWriter, r *http.Request) {
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil {
return res
}
+ // upsert mfa algorithm
+ if res := tx.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "config_id"}},
+ DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
+ }).Create(&store.Config{ConfigID: CNFMFAAlgorithm, StrValue: mfaAlgorithm}).Error; res != nil {
+ return res
+ }
return nil
})
if err != nil {
diff --git a/net/server/internal/api_setMultiFactorAuth.go b/net/server/internal/api_setMultiFactorAuth.go
index 2c8cf68f7..6c656f1aa 100644
--- a/net/server/internal/api_setMultiFactorAuth.go
+++ b/net/server/internal/api_setMultiFactorAuth.go
@@ -35,32 +35,37 @@ func SetMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
return;
}
- opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
+ mfaAlgorithm := APPMFASHA1
+ opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1}
if valid, _ := totp.ValidateCustom(code, account.MFASecret, time.Now(), opts); !valid {
- err := store.DB.Transaction(func(tx *gorm.DB) error {
- if account.MFAFailedTime + APPMFAFailPeriod > curTime {
- account.MFAFailedCount += 1
- if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
- return res
- }
- } else {
- account.MFAFailedTime = curTime
- if res := tx.Model(account).Update("mfa_failed_time", account.MFAFailedTime).Error; res != nil {
- return res
- }
- account.MFAFailedCount = 1
- if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
- return res
+ mfaAlgorithm = APPMFASHA256
+ opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
+ if valid, _ := totp.ValidateCustom(code, account.MFASecret, time.Now(), opts); !valid {
+ err := store.DB.Transaction(func(tx *gorm.DB) error {
+ if account.MFAFailedTime + APPMFAFailPeriod > curTime {
+ account.MFAFailedCount += 1
+ if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
+ return res
+ }
+ } else {
+ account.MFAFailedTime = curTime
+ if res := tx.Model(account).Update("mfa_failed_time", account.MFAFailedTime).Error; res != nil {
+ return res
+ }
+ account.MFAFailedCount = 1
+ if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
+ return res
+ }
}
+ return nil
+ })
+ if err != nil {
+ LogMsg("failed to increment fail count");
}
- return nil
- })
- if err != nil {
- LogMsg("failed to increment fail count");
- }
- ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
- return
+ ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
+ return
+ }
}
err = store.DB.Transaction(func(tx *gorm.DB) error {
@@ -69,6 +74,11 @@ func SetMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
ErrResponse(w, http.StatusInternalServerError, res)
return res
}
+ account.MFAAlgorithm = mfaAlgorithm;
+ if res := tx.Model(account).Update("mfa_algorithm", account.MFAAlgorithm).Error; res != nil {
+ ErrResponse(w, http.StatusInternalServerError, res)
+ return res
+ }
account.AccountRevision += 1;
if res := tx.Model(&account).Update("account_revision", account.AccountRevision).Error; res != nil {
return res
diff --git a/net/server/internal/api_setPushEvent.go b/net/server/internal/api_setPushEvent.go
index f03e4723b..cdbac04c4 100644
--- a/net/server/internal/api_setPushEvent.go
+++ b/net/server/internal/api_setPushEvent.go
@@ -121,9 +121,8 @@ func SendPushEvent(account store.Account, event string) {
if pushToken == "" || pushToken == "null" {
continue;
}
- url := "https://fcm.googleapis.com/fcm/send"
- payload := Payload{ Title: messageTitle, Body: messageBody, Sound: "default" };
- message := Message{ Notification: payload, To: pushToken };
+ url := "https://repeater.coredb.org/notify"
+ message := PushMessage{ Title: messageTitle, Body: messageBody, Token: pushToken };
body, err := json.Marshal(message)
if err != nil {
@@ -136,7 +135,6 @@ func SendPushEvent(account store.Account, event string) {
continue
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
- req.Header.Set("Authorization", "key=AAAAkgDXt8c:APA91bEjH67QpUWU6uAfCIXLqm0kf6AdPNVICZPCcWbmgW9NGYIErAxMDTy4LEbe4ik93Ho4Z-AJNIhr6nXXKC9qKmyKkkYHJWAEVH47_FXBQV6rsoi9ZB_oiuV66XKKAy1V40GmvfaX")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
diff --git a/net/server/internal/appValues.go b/net/server/internal/appValues.go
index e16eae202..2701ae8ea 100644
--- a/net/server/internal/appValues.go
+++ b/net/server/internal/appValues.go
@@ -156,6 +156,12 @@ const APPMFAFailPeriod = 300
//APPMFAFailCount limit of login failures in period
const APPMFAFailCount = 4
+//APPMFASHA256 internal mfa algorithm sha256
+const APPMFASHA256 = "sha256"
+
+//APPMFASHA1 internal mfa alogirthm sha1
+const APPMFASHA1 = "sha1"
+
//AppCardStatus compares cards status with string
func AppCardStatus(status string) bool {
if status == APPCardPending {
diff --git a/net/server/internal/configUtil.go b/net/server/internal/configUtil.go
index 3502e7056..7a0f2ed61 100644
--- a/net/server/internal/configUtil.go
+++ b/net/server/internal/configUtil.go
@@ -78,6 +78,9 @@ const CNFMFAEnabled = "mfa_enabled"
//CNFMFAConfirmed specified if mfa has been confirmed for admin
const CNFMFAConfirmed = "mfa_confirmed"
+//CNFMFAAlgorirthm specifies internal mfa alogirhtm to use
+const CNFMFAAlgorithm = "mfa_algorithm"
+
//CNFMFASecret specified the mfa secret
const CNFMFASecret = "mfa_secret"
diff --git a/net/server/internal/models.go b/net/server/internal/models.go
index 56a2ebf5f..130716e16 100644
--- a/net/server/internal/models.go
+++ b/net/server/internal/models.go
@@ -619,3 +619,13 @@ type Ring struct {
IcePassword string `json:"icePassword"`
}
+
+type PushMessage struct {
+ Title string `json:"title"`
+ Body string `json:"body"`
+ Token string `json:"token"`
+}
+
+type PushResponse struct {
+ Message string `json:"message"`
+}
diff --git a/net/server/internal/store/schema.go b/net/server/internal/store/schema.go
index 3e095913b..334fc41b0 100644
--- a/net/server/internal/store/schema.go
+++ b/net/server/internal/store/schema.go
@@ -84,6 +84,7 @@ type Account struct {
MFAEnabled bool `gorm:"not null;default:false"`
MFAConfirmed bool `gorm:"not null;default:false"`
MFASecret string
+ MFAAlgorithm string
MFAFailedTime int64
MFAFailedCount uint
Forward string
diff --git a/net/container/transform/transform_acopy.sh b/net/transform/transform_acopy.sh
similarity index 100%
rename from net/container/transform/transform_acopy.sh
rename to net/transform/transform_acopy.sh
diff --git a/net/container/transform/transform_icopy.sh b/net/transform/transform_icopy.sh
similarity index 100%
rename from net/container/transform/transform_icopy.sh
rename to net/transform/transform_icopy.sh
diff --git a/net/container/transform/transform_ilg.sh b/net/transform/transform_ilg.sh
similarity index 100%
rename from net/container/transform/transform_ilg.sh
rename to net/transform/transform_ilg.sh
diff --git a/net/container/transform/transform_ithumb.sh b/net/transform/transform_ithumb.sh
similarity index 100%
rename from net/container/transform/transform_ithumb.sh
rename to net/transform/transform_ithumb.sh
diff --git a/net/container/transform/transform_vcopy.sh b/net/transform/transform_vcopy.sh
similarity index 100%
rename from net/container/transform/transform_vcopy.sh
rename to net/transform/transform_vcopy.sh
diff --git a/net/container/transform/transform_vhd.sh b/net/transform/transform_vhd.sh
similarity index 100%
rename from net/container/transform/transform_vhd.sh
rename to net/transform/transform_vhd.sh
diff --git a/net/container/transform/transform_vlq.sh b/net/transform/transform_vlq.sh
similarity index 100%
rename from net/container/transform/transform_vlq.sh
rename to net/transform/transform_vlq.sh
diff --git a/net/container/transform/transform_vsd.sh b/net/transform/transform_vsd.sh
similarity index 100%
rename from net/container/transform/transform_vsd.sh
rename to net/transform/transform_vsd.sh
diff --git a/net/container/transform/transform_vthumb.sh b/net/transform/transform_vthumb.sh
similarity index 100%
rename from net/container/transform/transform_vthumb.sh
rename to net/transform/transform_vthumb.sh
diff --git a/net/web/.dockerignore b/net/web/.dockerignore
new file mode 100644
index 000000000..b512c09d4
--- /dev/null
+++ b/net/web/.dockerignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/net/web/.gitignore b/net/web/.gitignore
new file mode 100644
index 000000000..b7dab5e9c
--- /dev/null
+++ b/net/web/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+build
\ No newline at end of file
diff --git a/net/web/Dockerfile.dev b/net/web/Dockerfile.dev
new file mode 100644
index 000000000..4f5164703
--- /dev/null
+++ b/net/web/Dockerfile.dev
@@ -0,0 +1,7 @@
+FROM node:22-alpine
+
+WORKDIR /app
+
+RUN npm install --global chokidar-cli
+
+ENV SHELL=/bin/sh
\ No newline at end of file
diff --git a/net/web/src/constants/Strings.js b/net/web/src/constants/Strings.js
index 93d7b7e8b..ae657ee3d 100644
--- a/net/web/src/constants/Strings.js
+++ b/net/web/src/constants/Strings.js
@@ -193,7 +193,7 @@ export const en = {
securedMessage: 'Sealed Message',
mfaTitle: 'Multi-Factor Authentication',
- mfaSteps: 'Store the SHA256 secret and confirm the verification code',
+ mfaSteps: 'Store the secret and confirm the verification code',
mfaError: 'verification code error',
mfaDisabled: 'verification temporarily disabled',
mfaConfirm: 'Confirm',
@@ -403,7 +403,7 @@ export const fr = {
sealedMessage: 'Message Sécurisé',
mfaTitle: 'Authentification Multi-Factor',
- mfaSteps: 'Enregistrez le secret SHA256 et confirmez le code de vérification',
+ mfaSteps: 'Enregistrez le secret et confirmez le code de vérification',
mfaEnter: 'Entrez votre code de vérification',
mfaError: 'erreur de code de vérification',
mfaDisabled: 'vérification temporairement désactivée',
@@ -612,7 +612,7 @@ export const sp = {
sealedMessage: 'Mensaje Seguro',
mfaTitle: 'Autenticación de Dos Factores',
- mfaSteps: 'Guarde el secreto SHA256 y confirme el código de verificación',
+ mfaSteps: 'Guarde el secreto y confirme el código de verificación',
mfaEnter: 'Ingresa tu código de verificación',
mfaError: 'error de código de verificación',
mfaDisabled: 'verificación temporalmente deshabilitada',
@@ -821,7 +821,7 @@ export const pt = {
sealedMessage: 'Mensagem Segura',
mfaTitle: 'Autenticação de Dois Fatores',
- mfaSteps: 'Salve o segredo SHA256 e confirme o código de verificação',
+ mfaSteps: 'Salve o segredo e confirme o código de verificação',
mfaEnter: 'Digite seu código de verificação',
mfaError: 'erro de código de verificação',
mfaDisabled: 'verificação temporariamente desativada',
@@ -1030,7 +1030,7 @@ export const de = {
sealedMessage: 'Gesicherte Nachricht',
mfaTitle: 'Zwei-Faktor-Authentifizierung',
- mfaSteps: 'Speichern Sie das SHA256-Geheimnis und bestätigen Sie den Bestätigungscode',
+ mfaSteps: 'Speichern Sie das Geheimnis und bestätigen Sie den Bestätigungscode',
mfaEnter: 'Geben Sie Ihren Bestätigungs-Code ein',
mfaError: 'Verifizierungscodefehler',
mfaDisabled: 'Verifizierung vorübergehend deaktiviert',
@@ -1239,7 +1239,7 @@ export const ru = {
sealedMessage: 'Защищенное Cообщение',
mfaTitle: 'Двухфакторная аутентификация',
- mfaSteps: 'Сохраните секрет SHA256 и подтвердите код подтверждения',
+ mfaSteps: 'Сохраните секрет и подтвердите код подтверждения',
mfaEnter: 'Введите Ваш верификационный код',
mfaError: 'ошибка проверочного кода',
mfaDisabled: 'проверка временно отключена',
@@ -1252,3 +1252,212 @@ export const ru = {
confirmDisable: 'Отключение двухфакторной аутентификации',
disablePrompt: 'Вы уверены, что хотите отключить двухфакторную аутентификацию?',
};
+
+export const el = {
+ code: 'el',
+ settings: 'Ρυθμίσεις',
+ contacts: 'Επαφές',
+ logout: 'Αποσύνδεση',
+ confirmLogout: 'Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;',
+ contactsUpdated: 'Η κατάσταση των επαφών ενημερώθηκε',
+ disconnected: 'Αποσυνδεθήκατε από το διακομιστή',
+ allDevices: 'Αποσύνδεση από όλες τις συσκευές',
+ ok: 'Εντάξει',
+ cancel: 'Ακύρωση',
+ enableNotifications: 'Ειδοποιήσεις',
+
+ new: 'Νέο',
+ newMessage: 'Νέο Μήνυμα',
+ topics: 'Συζητήσεις',
+ unsetSealing: 'Κατάργηση Κλειδιού Σφράγισης',
+ newTopic: 'Νέα Συζήτηση',
+
+ noContacts: 'Δεν υπάρχουν Επαφές',
+ noTopics: 'Δεν υπάρχουν Συζητήσεις',
+ noConnected: 'Δεν υπάρχουν Συνδεδεμένες Επαφές',
+ subjectOptional: 'Θέμα (προαιρετικό)',
+ members: 'Μέλη',
+ sealedTopic: 'Σφραγισμένο Θέμα',
+ start: 'Έναρξη',
+
+ communication: 'Επικοινωνία για τον Αποκεντρωμένο Ιστό',
+ setupProfile: 'Ρυθμίστε το προφίλ σας',
+ connectPeople: 'Συνδεθείτε με άτομα',
+ startConversation: 'Ξεκινήστε μια συνομιλία',
+
+ default: 'Προεπιλογή',
+ dark: 'Σκούρο',
+ light: 'Φωτεινό',
+
+ operationFailed: 'Η λειτουργία απέτυχε',
+ tryAgain: 'Παρακαλώ προσπαθήστε ξανά.',
+
+ add: 'Προσθήκη',
+ save: 'Αποθήκευση',
+ forget: 'Ξέχασε το',
+ unlock: 'Ξεκλείδωμα',
+ profile: 'Προφίλ',
+ application: 'Εφαρμογή',
+ account: 'Λογαριασμός',
+ name: 'Όνομα',
+ node: 'Κόμβος',
+ location: 'Τοποθεσία',
+ description: 'Περιγραφή',
+ timeFormat: 'Μορφή Ώρας',
+ dateFormat: 'Μορφή Ημερομηνίας',
+ theme: 'Θέμα',
+ language: 'Γλώσσα',
+ timeUs: '12ωρο',
+ timeEu: '24ωρο',
+ dateUs: 'μμ/ηη',
+ dateEu: 'ηη/μμ',
+ registry: 'Ορατός λογαριασμός στο Μητρώο Διακομιστή',
+ sealedTopics: 'Σφραγισμένα Θέματα',
+ changeLogin: 'Αλλαγή Σύνδεσης',
+ selectImage: 'Επιλογή',
+ profileImage: 'Εικόνα Προφίλ',
+ profileDetails: 'Λεπτομέρειες Προφίλ',
+ enableSealed: 'Ενεργοποίηση Σφραγισμένων Συζητήσεων',
+ password: 'Κωδικός Πρόσβασης',
+ newPassword: 'Νέος Κωδικός Πρόσβασης',
+ confirmPassword: 'Επιβεβαίωση Κωδικού Πρόσβασης',
+ deleteKey: "Πληκτρολογήστε 'delete' για να αφαιρέσετε το κλειδί",
+ delete: 'διαγραφή',
+ remove: 'Διαγραφή',
+ username: 'Όνομα Χρήστη',
+ updateProfile: 'Ενημέρωση Προφίλ',
+
+ syncError: 'Σφάλμα Συγχρονισμού',
+ callTip: 'Κλήση Επαφής',
+ messageTip: 'Μήνυμα Επαφής',
+ connectedTip: 'Συνδεδεμένη Επαφή',
+ requestedTip: 'Αίτημα Σύνδεσης από Επαφή',
+ connectingTip: 'Αίτημα Σύνδεσης',
+ pendingTip: 'Αίτημα Σύνδεσης από Άγνωστη Επαφή',
+ confirmedTip: 'Αποσυνδεδεμένη Επαφή',
+ unsavedTip: 'Άγνωστη Επαφή',
+
+ actions: 'Ενέργειες',
+ resync: 'Εκ νέου συγχρονισμός',
+ connect: 'Σύνδεση',
+ disconnect: 'Αποσύνδεση',
+ disconnectContact: 'Αποσύνδεση Επαφής',
+ deleteContact: 'Διαγραφή Επαφής',
+ saveContact: 'Αποθήκευση Επαφής',
+ saveAccept: 'Αποθήκευση και Αποδοχή Σύνδεσης',
+ saveRequest: 'Αποθήκευση και Αίτημα Σύνδεσης',
+ ignoreRequest: 'Αγνόηση Αιτήματος',
+ acceptConnection: 'Αποδοχή Σύνδεσης',
+ requestConnection: 'Αίτημα Σύνδεσης',
+ cancelRequest: 'Ακύρωση Αιτήματος',
+ resyncContact: 'Εκ νέου συγχρονισμός Επαφής',
+
+ login: 'Σύνδεση',
+ create: 'Δημιουργία',
+ createAccount: 'Δημιουργία Λογαριασμού',
+ accountLogin: 'Σύνδεση Λογαριασμού',
+ toCreate: 'Οι λογαριασμοί δημιουργούνται μέσω συνδέσμου από τον πίνακα διαχειριστή.',
+ admin: 'Διαχειριστής',
+ loginError: 'Σφάλμα Σύνδεσης',
+ loginMessage: 'Παρακαλώ επιβεβαιώστε το όνομα χρήστη και τον κωδικό πρόσβασης.',
+ createError: 'Σφάλμα Δημιουργίας Λογαριασμού',
+ createMessage: 'Ελέγξτε το με τον διαχειριστή σας.',
+ adminError: 'Σφάλμα Πρόσβασης Διαχειριστή',
+ adminMessage: 'Παρακαλώ επιβεβαιώστε τον κωδικό πρόσβασης.',
+
+ confirmDelete: 'Διαγραφή Λογαριασμού',
+ areSure: 'Είστε σίγουροι ότι θέλετε να διαγράψετε το λογαριασμό;',
+
+ mb: 'MB',
+ gb: 'GB',
+ copied: 'Αντιγράφηκε',
+ accounts: 'Λογαριασμοί',
+ accessAccount: 'Πρόσβαση Λογαριασμού',
+ browserLink: 'Σύνδεσμος Περιηγητή',
+ mobileToken: 'Token Κινητού',
+ createLink: 'Δημιουργία Συνδέσμου Λογαριασμού',
+ configureServer: 'Διαμόρφωση Διακομιστή',
+ reloadAccounts: 'Επαναφόρτωση Λογαριασμών',
+ disableAccount: 'Απενεργοποίηση Λογαριασμού',
+ enableAccount: 'Ενεργοποίηση Λογαριασμού',
+ deleteAccount: 'Διαγραφή Λογαριασμού',
+ hostHint: 'domain:port/app',
+ federatedHost: 'Διεύθυνση Διακομιστή για Ομοσπονδιακή λειτουργία',
+ storageLimit: 'Όριο Αποθήκευσης (GB) / Λογαριασμό',
+ storageHint: '0 για Απεριόριστο',
+ keyType: 'Τύπος Κλειδιού Λογαριασμού',
+ accountCreation: 'Δυνατότητα δημιουργίας Δημόσιου Λογαριασμού',
+ enablePush: 'Ενεργοποίηση Ειδοποιήσεων',
+ allowUnsealed: 'Επιτρέψτε μη Σφραγισμένες Συζητήσεις',
+ topicContent: 'Περιεχόμενο Συζήτησης:',
+ enableImage: 'Ενεργοποίηση Ουράς Εικόνων',
+ imageHint: 'Επιτρέψτε την ανάρτηση εικόνων σε συζητήσεις',
+ enableAudio: 'Ενεργοποίηση Ουράς Ήχου',
+ audioHint: 'Επιτρέψτε την ανάρτηση ήχου σε συζητήσεις',
+ enableVideo: 'Ενεργοποίηση Ουράς Βίντεο',
+ videoHint: 'Επιτρέψτε την ανάρτηση βίντεο σε συζητήσεις',
+ enableBinary: 'Ενεργοποίηση Ψηφιακών Αρχείων',
+ binaryHint: 'Επιτρέψτε την ανάρτηση υπολοίπων ψηφιακών αρχείων σε συζητήσεις',
+ enableWeb: 'Ενεργοποίηση Κλήσεων WebRTC',
+ webHint: 'Ενεργοποίηση κλήσεων ήχου και βίντεο για τις επαφές',
+ enableService: 'Υπηρεσία Cloudflare',
+ serviceHint: 'Ενεργοποίηση Υπηρεσίας Cloudflare',
+ serverUrl: 'URL Διακομιστή WebRTC',
+ urlHint: 'turn:ip:port?transport=udp',
+ webUsername: 'Όνομα Χρήστη WebRTC',
+ webPassword: 'Κωδικός Πρόσβασης WebRTC',
+ failedLoad: 'Αποτυχία Φόρτωσης',
+ limit: 'Όριο',
+
+ deleteMessage: 'Διαγραφή Μηνύματος',
+ messageHint: 'Είστε σίγουροι ότι θέλετε να διαγράψετε το μήνυμα;',
+ attachImage: 'Επισύναψη Εικόνας',
+ attachVideo: 'Επισύναψη Βίντεο',
+ attachAudio: 'Επισύναψη Ήχου',
+ attachFile: 'Επισύναψη Αρχείου',
+ fontColor: 'Αλλαγή Χρώματος Γραμματοσειράς',
+ fontSize: 'Αλλαγή Μεγέθους Γραμματοσειράς',
+ postMessage: 'Ανάρτηση Μηνύματος',
+
+ close: 'Κλείσιμο',
+ leave: 'Αποχώρηση',
+ confirmTopic: 'Διαγραφή συζήτησης',
+ sureTopic: 'Είστε σίγουροι ότι θέλετε να διαγράψετε αυτήν τη συζήτηση;',
+ confirmLeave: 'Αποχώρηση από τη συζήτηση',
+ sureLeave: 'Είστε σίγουροι ότι θέλετε να αποχωρήσετε από αυτήν τη συζήτηση;',
+
+ details: 'Λεπτομέρειες',
+ host: 'Διακομιστής',
+ guest: 'Επισκέπτης',
+ editSubject: 'Επεξεργασία Θέματος',
+ editMembership: 'Επεξεργασία Μελών',
+ deleteTopic: 'Διαγραφή Συζήτησης',
+ leaveTopic: 'Αποχώρηση από τη συζήτηση',
+
+ integrated: 'Ενσωματωμένο',
+ microphone: 'Μικρόφωνο',
+ camera: 'Κάμερα',
+
+ notes: 'Σημειώσεις',
+
+ disconnecting: 'Αποσύνδεση Επαφής',
+ confirmDisconnect: 'Είστε σίγουροι ότι θέλετε να αποσυνδέσετε την επαφή;',
+ removing: 'Διαγραφή Επαφής',
+ confirmRemove: 'Είστε σίγουροι ότι θέλετε να διαγράψετε την επαφή;',
+ message: 'Μήνυμα',
+ securedMessage: 'Κρυπτογραφημένο Μήνυμα',
+
+ mfaTitle: 'Επαλήθευση Πολλαπλών Παραγόντων (MFA)',
+ mfaSteps: 'Αποθηκεύστε το μυστικό και επιβεβαιώστε τον κωδικό επαλήθευσης',
+ mfaError: 'σφάλμα κωδικού επαλήθευσης',
+ mfaDisabled: 'η επαλήθευση είναι προσωρινά απενεργοποιημένη',
+ mfaConfirm: 'Επιβεβαίωση',
+ mfaEnter: 'Εισάγετε τον κωδικό επαλήθευσης σας',
+
+ enableMultifactor: 'Ενεργοποίηση επαλήθευσης πολλαπλών παραγόντων (MFA)',
+ disableMultifactor: 'Απενεργοποίηση επαλήθευσης πολλαπλών παραγόντων (MFA)',
+
+ disable: 'Απενεργοποίηση',
+ confirmDisable: 'Απενεργοποίηση Επαλήθευσης Πολλαπλών Παραγόντων (MFA)',
+ disablePrompt: 'Είστε σίγουροι πως θέλετε να απενεργοποιήσετε την επαλήθευση πολλαπλών παραγόντων (MFA)',
+};
diff --git a/net/web/src/session/conversation/topicItem/TopicItem.jsx b/net/web/src/session/conversation/topicItem/TopicItem.jsx
index d2d9a5812..8bd8208ab 100644
--- a/net/web/src/session/conversation/topicItem/TopicItem.jsx
+++ b/net/web/src/session/conversation/topicItem/TopicItem.jsx
@@ -126,7 +126,7 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove, str
)}
{ !sealed && !state.editing && (
{ clickable }
; };