server i18n: move inline HTML to templates.go, localize all handler strings

- Admin & user dashboard HTML moved from handlers to templates.go with i18n.T()
- SafeVaultPath applied in sync_apply.go (note/file create/update, blob restore)
- DeleteNode/RenameNode/MoveNode fixed: correct activity type / entity variant
- Added TypeNoteDeleted, TypeNodeDeleted, TypeFolderMoved activity constants
- Added locale() helper on Server struct, removed hardcoded 'ru' in handlers
- Password policy loosened: 8-256 chars, any characters, machine-readable error codes
- check-i18n.sh: Go Cyrillic = FAIL with explicit exception list, Go locale key consistency check
This commit is contained in:
2026-06-02 11:26:54 +08:00
parent 2fa583d157
commit 7091397649
11 changed files with 435 additions and 259 deletions
+72 -9
View File
@@ -49,6 +49,37 @@ func (a *App) CreateNode(parentID, nodeType, title, section string) (*NodeDTO, e
}
func (a *App) DeleteNode(id string) error {
n, err := a.nodes.GetActive(id)
if err != nil {
return a.nodes.SoftDelete(id)
}
pid := ""
if n.ParentID != nil {
pid = *n.ParentID
}
var entity string
var targetType string
var evType string
switch n.Type {
case nodes.TypeNote:
entity = syncsvc.EntityNote
targetType = activity.TargetNote
evType = activity.TypeNoteDeleted
case nodes.TypeFolder:
entity = syncsvc.EntityFolder
targetType = activity.TargetFolder
evType = activity.TypeFolderDeleted
case nodes.TypeFile:
entity = syncsvc.EntityFile
targetType = activity.TargetFile
evType = activity.TypeFileDeleted
default:
entity = syncsvc.EntityNode
targetType = activity.TargetNode
evType = activity.TypeNodeDeleted
}
_ = a.activity.Record(pid, targetType, id, "", evType, n.Title, "")
_ = a.sync.RecordOp(entity, id, syncsvc.OpDelete, nil)
return a.nodes.SoftDelete(id)
}
@@ -65,17 +96,28 @@ func (a *App) RenameNode(nodeID, newTitle string) error {
if n.ParentID != nil {
pid = *n.ParentID
}
evType := activity.TypeFileRenamed
targetType := activity.TargetFile
if n.Type == nodes.TypeFolder {
var evType string
var targetType string
var syncEntity string
switch n.Type {
case nodes.TypeNote:
evType = activity.TypeNoteUpdated
targetType = activity.TargetNote
syncEntity = syncsvc.EntityNote
case nodes.TypeFile:
evType = activity.TypeFileRenamed
targetType = activity.TargetFile
syncEntity = syncsvc.EntityFile
case nodes.TypeFolder:
evType = activity.TypeFolderRenamed
targetType = activity.TargetFolder
syncEntity = syncsvc.EntityFolder
default:
evType = activity.TypeNodeUpdated
targetType = activity.TargetNode
syncEntity = syncsvc.EntityNode
}
_ = a.activity.Record(pid, targetType, nodeID, "", evType, newTitle, `{"from":"`+oldTitle+`","to":"`+newTitle+`"}`)
syncEntity := syncsvc.EntityFile
if n.Type == nodes.TypeFolder {
syncEntity = syncsvc.EntityFolder
}
_ = a.sync.RecordOp(syncEntity, nodeID, syncsvc.OpUpdate, map[string]interface{}{
"title": newTitle,
"updated_at": time.Now().UTC().Format(time.RFC3339),
@@ -108,8 +150,29 @@ func (a *App) MoveNode(nodeID, newParentID string) error {
if node.ParentID != nil {
pid = *node.ParentID
}
_ = a.activity.Record(pid, activity.TargetFile, nodeID, "", activity.TypeFileMoved, node.Title, `{"to":"`+newParentID+`"}`)
_ = a.sync.RecordOp(syncsvc.EntityFile, nodeID, syncsvc.OpMove, map[string]interface{}{
var targetType string
var evType string
var syncEntity string
switch node.Type {
case nodes.TypeNote:
targetType = activity.TargetNote
evType = activity.TypeNoteUpdated
syncEntity = syncsvc.EntityNote
case nodes.TypeFile:
targetType = activity.TargetFile
evType = activity.TypeFileMoved
syncEntity = syncsvc.EntityFile
case nodes.TypeFolder:
targetType = activity.TargetFolder
evType = activity.TypeFolderMoved
syncEntity = syncsvc.EntityFolder
default:
targetType = activity.TargetNode
evType = activity.TypeNodeUpdated
syncEntity = syncsvc.EntityNode
}
_ = a.activity.Record(pid, targetType, nodeID, "", evType, node.Title, `{"to":"`+newParentID+`"}`)
_ = a.sync.RecordOp(syncEntity, nodeID, syncsvc.OpMove, map[string]interface{}{
"parent_id": newParentID,
"updated_at": time.Now().UTC().Format(time.RFC3339),
})
+18 -2
View File
@@ -193,14 +193,26 @@ func (a *App) applyRemoteNoteCreate(op syncsvc.Op) error {
}
}
dest := filepath.Join(a.vault, payload.Path)
var dest string
if payload.Path == "" {
filename := payload.Filename
if filename == "" {
filename = payload.NodeID[:8] + ".md"
} else {
cleanFilename, err := syncsvc.SafeVaultPath(a.vault, filename)
if err != nil {
return fmt.Errorf("unsafe path in %s: %w", op.EntityType, err)
}
filename = cleanFilename
}
dest = filepath.Join(a.vault, "spaces", filename)
payload.Path, _ = filepath.Rel(a.vault, dest)
} else {
cleanPath, err := syncsvc.SafeVaultPath(a.vault, payload.Path)
if err != nil {
return fmt.Errorf("unsafe path in %s: %w", op.EntityType, err)
}
dest = filepath.Join(a.vault, cleanPath)
}
if err := os.MkdirAll(filepath.Dir(dest), 0o750); err != nil {
return err
@@ -354,7 +366,11 @@ func (a *App) applyRemoteFileCreate(op syncsvc.Op) error {
}
}
dest := filepath.Join(a.vault, payload.Path)
cleanPath, pathErr := syncsvc.SafeVaultPath(a.vault, payload.Path)
if pathErr != nil {
return fmt.Errorf("unsafe path in file: %w", pathErr)
}
dest := filepath.Join(a.vault, cleanPath)
if err := os.MkdirAll(filepath.Dir(dest), 0o750); err == nil {
input, rErr := os.ReadFile(blobPath)
if rErr == nil {