followup: SafeVaultPath in note update, email i18n, strict check-i18n.sh

- applyRemoteNoteUpdate: use SafeVaultPath for vault mode, skip non-vault with log
- Email subjects/bodies moved to Go i18n (confirm + reset) in ru.json and en.json
- check-i18n.sh: ru/en key mismatch now FAIL (not WARNING)
- All builds, tests, frontend pass
This commit is contained in:
mirivlad 2026-06-02 11:40:27 +08:00
parent 7091397649
commit 12f2916a24
6 changed files with 50 additions and 31 deletions

View File

@ -269,25 +269,28 @@ func (a *App) applyRemoteNoteUpdate(op syncsvc.Op) error {
return fmt.Errorf("note record not found: %w", err) return fmt.Errorf("note record not found: %w", err)
} }
var abs string
if storageMode == "vault" { if storageMode == "vault" {
abs = filepath.Join(a.vault, filePath) abs, err := syncsvc.SafeVaultPath(a.vault, filePath)
} else { if err != nil {
abs = filePath return fmt.Errorf("unsafe vault path in note update: %w", err)
}
if err := os.WriteFile(abs, []byte(payload.Content), 0o640); err != nil {
return err
}
info, _ := os.Stat(abs)
size := int64(0)
if info != nil {
size = info.Size()
}
now := time.Now().UTC().Format(time.RFC3339)
_, e := a.db.Exec(
`UPDATE files SET size=?, updated_at=? WHERE path=? AND storage_mode=?`,
size, now, filePath, storageMode)
return e
} }
if err := os.WriteFile(abs, []byte(payload.Content), 0o640); err != nil { log.Printf("applyRemoteNoteUpdate: skipping non-vault note update for node %s (mode=%s, path=%s)",
return err payload.NodeID, storageMode, filePath)
} return nil
info, _ := os.Stat(abs)
size := int64(0)
if info != nil {
size = info.Size()
}
now := time.Now().UTC().Format(time.RFC3339)
_, e := a.db.Exec(
`UPDATE files SET size=?, updated_at=? WHERE path=? AND storage_mode=?`,
size, now, filePath, storageMode)
return e
} }
func (a *App) applyRemoteFileOrFolderOp(op syncsvc.Op) error { func (a *App) applyRemoteFileOrFolderOp(op syncsvc.Op) error {

View File

@ -15,6 +15,8 @@ import (
"strings" "strings"
"time" "time"
"verstak/internal/i18n"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -82,8 +84,8 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
} else { } else {
confirmURL = fmt.Sprintf("/api/v1/auth/confirm?token=%s", tokenStr) confirmURL = fmt.Sprintf("/api/v1/auth/confirm?token=%s", tokenStr)
} }
body := fmt.Sprintf("Welcome to Verstak Sync!\n\nPlease confirm your email by clicking:\n%s\n\nIf you did not register, ignore this message.", confirmURL) body := fmt.Sprintf(i18n.T(s.locale(), "server.emailConfirmBody"), confirmURL)
if err := s.smtpSend(req.Email, "Confirm your Verstak Sync account", body); err != nil { if err := s.smtpSend(req.Email, i18n.T(s.locale(), "server.emailConfirmSubject"), body); err != nil {
log.Printf("register: failed to send confirm email: %v", err) log.Printf("register: failed to send confirm email: %v", err)
} }
} else { } else {
@ -199,8 +201,8 @@ func (s *Server) handleForgot(w http.ResponseWriter, r *http.Request) {
if srvURL != "" { if srvURL != "" {
resetURL = fmt.Sprintf("%s/api/v1/auth/reset?token=%s", srvURL, tokenStr) resetURL = fmt.Sprintf("%s/api/v1/auth/reset?token=%s", srvURL, tokenStr)
} }
body := fmt.Sprintf("Reset your Verstak Sync password:\n\n%s\n\nThis link expires in 1 hour.", resetURL) body := fmt.Sprintf(i18n.T(s.locale(), "server.emailResetBody"), resetURL)
s.smtpSend(req.Email, "Verstak Sync password reset", body) s.smtpSend(req.Email, i18n.T(s.locale(), "server.emailResetSubject"), body)
} }
jsonOK(w, map[string]string{"status": "if email exists, reset link sent"}) jsonOK(w, map[string]string{"status": "if email exists, reset link sent"})
} }

View File

@ -98,8 +98,8 @@ func (s *Server) handleUserWebRegister(w http.ResponseWriter, r *http.Request) {
} else { } else {
confirmURL = fmt.Sprintf("http://%s/api/v1/auth/confirm?token=%s", r.Host, tokenStr) confirmURL = fmt.Sprintf("http://%s/api/v1/auth/confirm?token=%s", r.Host, tokenStr)
} }
body := fmt.Sprintf("Welcome to Verstak Sync!\n\nPlease confirm your email by clicking:\n%s\n\nIf you did not register, ignore this message.", confirmURL) body := fmt.Sprintf(i18n.T(s.locale(), "server.emailConfirmBody"), confirmURL)
if err := s.smtpSend(email, "Confirm your Verstak Sync account", body); err != nil { if err := s.smtpSend(email, i18n.T(s.locale(), "server.emailConfirmSubject"), body); err != nil {
log.Printf("register web: failed to send confirm email: %v", err) log.Printf("register web: failed to send confirm email: %v", err)
} }
} else { } else {
@ -153,8 +153,8 @@ func (s *Server) handleUserWebForgot(w http.ResponseWriter, r *http.Request) {
if srvURL != "" { if srvURL != "" {
resetURL = fmt.Sprintf("%s/reset?token=%s", srvURL, tokenStr) resetURL = fmt.Sprintf("%s/reset?token=%s", srvURL, tokenStr)
} }
body := fmt.Sprintf("Reset your Verstak Sync password:\n\n%s\n\nThis link expires in 1 hour.", resetURL) body := fmt.Sprintf(i18n.T(s.locale(), "server.emailResetBody"), resetURL)
if err := s.smtpSend(email, "Verstak Sync password reset", body); err != nil { if err := s.smtpSend(email, i18n.T(s.locale(), "server.emailResetSubject"), body); err != nil {
log.Printf("forgot web: failed to send reset email: %v", err) log.Printf("forgot web: failed to send reset email: %v", err)
} }
} else { } else {

View File

@ -97,5 +97,10 @@
"error.accountBlocked": "Account blocked", "error.accountBlocked": "Account blocked",
"error.emailNotConfirmed": "Email not confirmed", "error.emailNotConfirmed": "Email not confirmed",
"error.tokenInvalid": "Invalid or expired token", "error.tokenInvalid": "Invalid or expired token",
"error.tokenExpired": "Token expired" "error.tokenExpired": "Token expired",
"server.emailConfirmSubject": "Confirm your Verstak Sync account",
"server.emailConfirmBody": "Welcome to Verstak Sync!\n\nPlease confirm your email by clicking:\n%s\n\nIf you did not register, ignore this message.",
"server.emailResetSubject": "Verstak Sync password reset",
"server.emailResetBody": "Reset your Verstak Sync password:\n\n%s\n\nThis link expires in 1 hour."
} }

View File

@ -379,5 +379,10 @@
"error.accountBlocked": "Аккаунт заблокирован", "error.accountBlocked": "Аккаунт заблокирован",
"error.emailNotConfirmed": "Email не подтверждён", "error.emailNotConfirmed": "Email не подтверждён",
"error.tokenInvalid": "Неверный или просроченный токен", "error.tokenInvalid": "Неверный или просроченный токен",
"error.tokenExpired": "Срок действия токена истёк" "error.tokenExpired": "Срок действия токена истёк",
"server.emailConfirmSubject": "Подтверждение аккаунта Verstak Sync",
"server.emailConfirmBody": "Добро пожаловать в Verstak Sync!\n\nПодтвердите email, перейдя по ссылке:\n%s\n\nЕсли вы не регистрировались, проигнорируйте это письмо.",
"server.emailResetSubject": "Сброс пароля Verstak Sync",
"server.emailResetBody": "Сброс пароля Verstak Sync:\n\n%s\n\nСсылка действительна 1 час."
} }

View File

@ -97,12 +97,14 @@ MISSING_EN=$(comm -23 <(echo "$RU_KEYS") <(echo "$EN_KEYS"))
MISSING_RU=$(comm -23 <(echo "$EN_KEYS") <(echo "$RU_KEYS")) MISSING_RU=$(comm -23 <(echo "$EN_KEYS") <(echo "$RU_KEYS"))
if [ -n "$MISSING_EN" ]; then if [ -n "$MISSING_EN" ]; then
echo "WARNING: Keys in frontend ru.js but missing in en.js:" echo "FAIL: Keys in frontend ru.js but missing in en.js:"
echo "$MISSING_EN" echo "$MISSING_EN"
EXIT=1
fi fi
if [ -n "$MISSING_RU" ]; then if [ -n "$MISSING_RU" ]; then
echo "WARNING: Keys in frontend en.js but missing in ru.js:" echo "FAIL: Keys in frontend en.js but missing in ru.js:"
echo "$MISSING_RU" echo "$MISSING_RU"
EXIT=1
fi fi
if [ -z "$MISSING_EN" ] && [ -z "$MISSING_RU" ]; then if [ -z "$MISSING_EN" ] && [ -z "$MISSING_RU" ]; then
echo "OK: All frontend locale keys match between ru.js and en.js" echo "OK: All frontend locale keys match between ru.js and en.js"
@ -116,12 +118,14 @@ GO_MISSING_EN=$(comm -23 <(echo "$GO_RU_KEYS") <(echo "$GO_EN_KEYS"))
GO_MISSING_RU=$(comm -23 <(echo "$GO_EN_KEYS") <(echo "$GO_RU_KEYS")) GO_MISSING_RU=$(comm -23 <(echo "$GO_EN_KEYS") <(echo "$GO_RU_KEYS"))
if [ -n "$GO_MISSING_EN" ]; then if [ -n "$GO_MISSING_EN" ]; then
echo "WARNING: Keys in Go ru.json but missing in en.json:" echo "FAIL: Keys in Go ru.json but missing in en.json:"
echo "$GO_MISSING_EN" echo "$GO_MISSING_EN"
EXIT=1
fi fi
if [ -n "$GO_MISSING_RU" ]; then if [ -n "$GO_MISSING_RU" ]; then
echo "WARNING: Keys in Go en.json but missing in ru.json:" echo "FAIL: Keys in Go en.json but missing in ru.json:"
echo "$GO_MISSING_RU" echo "$GO_MISSING_RU"
EXIT=1
fi fi
if [ -z "$GO_MISSING_EN" ] && [ -z "$GO_MISSING_RU" ]; then if [ -z "$GO_MISSING_EN" ] && [ -z "$GO_MISSING_RU" ]; then
echo "OK: All Go locale keys match between ru.json and en.json" echo "OK: All Go locale keys match between ru.json and en.json"