Implement AsciiDoc and TipTap conversion logic, including new Admonition node and CodeBlock extension; add DiffViewer and HistoryPanel components for document version comparison; introduce password hashing utility with SHA-256.

This commit is contained in:
2026-04-12 22:28:15 +02:00
parent 05b773c14c
commit 376b946e73
22 changed files with 2767 additions and 323 deletions

View File

@@ -64,6 +64,11 @@ func (s *Server) initRuntime(cfg *config.Config) {
}
var d *db.DB
if dir := filepath.Dir(cfg.DBPath); dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("[db] failed to create directory %s: %v", dir, err)
}
}
if database, err := db.New(cfg.DBPath); err != nil {
log.Printf("[db] failed to open at %s: %v", cfg.DBPath, err)
} else {
@@ -127,8 +132,11 @@ func (s *Server) handleGraphQL(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
cfg := s.cfg
store := s.store
database := s.database
s.mu.RUnlock()
needsSetup := cfg == nil || database == nil || !database.HasUsers()
q := req.Query
switch {
@@ -137,7 +145,7 @@ func (s *Server) handleGraphQL(w http.ResponseWriter, r *http.Request) {
s.handleSetup(w, req)
case strings.Contains(q, "systemStatus"):
if cfg == nil {
if needsSetup {
writeJSON(w, `{"data":{"systemStatus":"REQUIRE_SETUP"}}`)
} else {
writeJSON(w, `{"data":{"systemStatus":"OK"}}`)
@@ -155,7 +163,7 @@ func (s *Server) handleGraphQL(w http.ResponseWriter, r *http.Request) {
// ── All others require initialised config ─────────────────────────────────
default:
if cfg == nil {
if needsSetup {
writeGQLError(w, "REQUIRE_SETUP")
return
}
@@ -201,17 +209,23 @@ func (s *Server) dispatchAuthenticated(
case strings.Contains(q, "config"):
s.handleConfig(w, sess)
case strings.Contains(q, "history"):
s.handleHistory(w, req, sess)
case strings.Contains(q, "repoStatus"):
s.handleRepoStatus(w, sess)
case strings.Contains(q, "diff"):
s.handleDiff(w, req, sess)
case strings.Contains(q, "initCommit"):
s.handleInitCommit(w, req, sess)
case strings.Contains(q, "documentAtCommit"):
s.handleDocumentAtCommit(w, req, sess)
case strings.Contains(q, "history"):
s.handleHistory(w, req, sess)
case strings.Contains(q, "documents") || strings.Contains(q, "document"):
s.handleDocuments(w, req, sess, store)
case strings.Contains(q, "diff"):
s.handleDiff(w, req, sess)
case strings.Contains(q, "documentAtCommit"):
s.handleDocumentAtCommit(w, req, sess)
case strings.Contains(q, "documents") || strings.Contains(q, "document"):
s.handleDocuments(w, req, sess, store)
default:
writeJSON(w, `{"data":{"systemStatus":"OK"}}`)
@@ -826,6 +840,63 @@ func (s *Server) handleConfig(w http.ResponseWriter, sess *auth.Session) {
})
}
func (s *Server) handleRepoStatus(w http.ResponseWriter, sess *auth.Session) {
if sess == nil {
writeGQLError(w, "UNAUTHORIZED")
return
}
s.mu.RLock()
repo := s.gitRepo
s.mu.RUnlock()
hasUncommitted := repo != nil && repo.HasUncommitted()
writeJSONObj(w, map[string]interface{}{
"data": map[string]interface{}{
"repoStatus": map[string]interface{}{
"hasUncommitted": hasUncommitted,
},
},
})
}
func (s *Server) handleInitCommit(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
if sess == nil {
writeGQLError(w, "UNAUTHORIZED")
return
}
message, _ := req.Variables["message"].(string)
if message == "" {
message = "Initial commit"
}
s.mu.RLock()
repo := s.gitRepo
s.mu.RUnlock()
if repo == nil {
writeGQLError(w, "storage not initialised")
return
}
if !repo.HasUncommitted() {
// Nothing to commit — return success without error.
writeJSON(w, `{"data":{"initCommit":true}}`)
return
}
email := sess.Username + "@archivum"
if err := repo.CommitAll(message, sess.Username, email); err != nil {
log.Printf("[git] initCommit failed for %s: %v", sess.Username, err)
writeGQLError(w, fmt.Sprintf("commit failed: %v", err))
return
}
log.Printf("[git] initCommit by %s: %q", sess.Username, message)
writeJSON(w, `{"data":{"initCommit":true}}`)
}
func (s *Server) handleUpdateStoragePath(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
if sess == nil {
log.Printf("[unauth] session is nil")
@@ -886,7 +957,7 @@ func (s *Server) handleSetup(w http.ResponseWriter, req gqlRequest) {
cfg := &config.Config{
StoragePath: storagePath,
DBPath: "/data/db/archivum.db",
DBPath: resolveDBPath(s.configPath),
JWTSecret: jwtSecret,
ListenAddr: ":4000",
}
@@ -905,19 +976,8 @@ func (s *Server) handleSetup(w http.ResponseWriter, req gqlRequest) {
}
}
if err := os.MkdirAll(dirOf(s.configPath), 0755); err != nil {
writeGQLError(w, fmt.Sprintf("could not create config directory: %v", err))
return
}
if err := config.Save(s.configPath, cfg); err != nil {
writeGQLError(w, fmt.Sprintf("failed to save config: %v", err))
return
}
log.Printf("[setup] config written to %s", s.configPath)
// Open database and create admin user.
// Open database and create admin user BEFORE saving config,
// so a failed DB creation does not leave a broken config.json on disk.
if err := os.MkdirAll(dirOf(cfg.DBPath), 0755); err != nil {
writeGQLError(w, fmt.Sprintf("could not create db directory: %v", err))
return
@@ -942,6 +1002,18 @@ func (s *Server) handleSetup(w http.ResponseWriter, req gqlRequest) {
log.Printf("[setup] admin user %q created", adminUser)
if err := os.MkdirAll(dirOf(s.configPath), 0755); err != nil {
writeGQLError(w, fmt.Sprintf("could not create config directory: %v", err))
return
}
if err := config.Save(s.configPath, cfg); err != nil {
writeGQLError(w, fmt.Sprintf("failed to save config: %v", err))
return
}
log.Printf("[setup] config written to %s", s.configPath)
store, err := storage.New(storagePath)
if err != nil {
log.Printf("[setup] failed to open storage at %s: %v", storagePath, err)
@@ -1144,6 +1216,17 @@ func dirOf(path string) string {
return path[:idx]
}
// resolveDBPath returns the best DB path given the config file location.
// Prefers /data/db/archivum.db (Docker volume) if /data is writable;
// otherwise places the DB next to the config file.
func resolveDBPath(configPath string) string {
const dockerDB = "/data/db/archivum.db"
if err := os.MkdirAll("/data/db", 0755); err == nil {
return dockerDB
}
return filepath.Join(dirOf(configPath), "archivum.db")
}
func (s *Server) handleHistory(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
@@ -1174,6 +1257,8 @@ func (s *Server) handleHistory(w http.ResponseWriter, req gqlRequest, sess *auth
"email": entry.Email,
"date": entry.Date,
"subject": entry.Subject,
"added": entry.Added,
"removed": entry.Removed,
})
}
writeJSONObj(w, map[string]interface{}{ "data": map[string]interface{}{ "history": out } })