fix: code hardening from senior dev audit #40
@@ -0,0 +1,27 @@
|
|||||||
|
# Code Hardening — Senior Dev Audit Fixes
|
||||||
|
|
||||||
|
## Phase 1: Quick Wins (safety + correctness)
|
||||||
|
- [ ] Cap API list limit at 200
|
||||||
|
- [ ] Fix markdown XSS — add DOMPurify to sanitize marked output
|
||||||
|
- [ ] Add missing DB indexes (deleted_at, modified_at) via v4 migration
|
||||||
|
- [ ] Fix v2 migration error handling (swallowed ALTER TABLE errors)
|
||||||
|
- [ ] Fix ~/.nib directory permissions (0o755 → 0o700)
|
||||||
|
|
||||||
|
## Phase 2: CI Pipeline
|
||||||
|
- [ ] Gitea Actions workflow: test + lint on PR
|
||||||
|
|
||||||
|
## Phase 3: context.Context in Store
|
||||||
|
- [ ] Thread context.Context through all Store methods
|
||||||
|
- [ ] Use context in API handlers (from r.Context())
|
||||||
|
- [ ] Use context in CLI commands (cobra context)
|
||||||
|
|
||||||
|
## Phase 4: cmd/ Tests
|
||||||
|
- [ ] Test add command
|
||||||
|
- [ ] Test ls command
|
||||||
|
- [ ] Test promote/demote commands
|
||||||
|
- [ ] Test delete command
|
||||||
|
- [ ] Test absorb command
|
||||||
|
|
||||||
|
## Phase 5: Backup/Export
|
||||||
|
- [ ] nib export — dump entities to JSON
|
||||||
|
- [ ] nib backup — safe SQLite backup (handles WAL)
|
||||||
@@ -92,6 +92,9 @@ func listEntities(store *db.Store) http.HandlerFunc {
|
|||||||
writeError(w, http.StatusBadRequest, "invalid_input", "limit must be a positive integer")
|
writeError(w, http.StatusBadRequest, "invalid_input", "limit must be a positive integer")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
p.Limit = limit
|
p.Limit = limit
|
||||||
}
|
}
|
||||||
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
||||||
|
|||||||
+21
-4
@@ -51,7 +51,7 @@ func (s *Store) Close() error {
|
|||||||
return s.db.Close()
|
return s.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSchema = 3
|
const currentSchema = 4
|
||||||
|
|
||||||
var migrations = []func(db *sql.DB) error{
|
var migrations = []func(db *sql.DB) error{
|
||||||
// v1: initial schema
|
// v1: initial schema
|
||||||
@@ -92,8 +92,12 @@ var migrations = []func(db *sql.DB) error{
|
|||||||
|
|
||||||
// v2: add title and description columns
|
// v2: add title and description columns
|
||||||
func(db *sql.DB) error {
|
func(db *sql.DB) error {
|
||||||
db.Exec(`ALTER TABLE entities ADD COLUMN title TEXT`)
|
if _, err := db.Exec(`ALTER TABLE entities ADD COLUMN title TEXT`); err != nil {
|
||||||
db.Exec(`ALTER TABLE entities ADD COLUMN description TEXT`)
|
return fmt.Errorf("add title column: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`ALTER TABLE entities ADD COLUMN description TEXT`); err != nil {
|
||||||
|
return fmt.Errorf("add description column: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -166,6 +170,19 @@ var migrations = []func(db *sql.DB) error{
|
|||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// v4: add indexes for common query filters
|
||||||
|
func(db *sql.DB) error {
|
||||||
|
for _, idx := range []string{
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_entities_deleted ON entities(deleted_at)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_entities_modified ON entities(modified_at DESC) WHERE deleted_at IS NULL`,
|
||||||
|
} {
|
||||||
|
if _, err := db.Exec(idx); err != nil {
|
||||||
|
return fmt.Errorf("create index: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) migrate() error {
|
func (s *Store) migrate() error {
|
||||||
@@ -200,7 +217,7 @@ func DefaultPath() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
dir := filepath.Join(home, ".nib")
|
dir := filepath.Join(home, ".nib")
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return filepath.Join(dir, "nib.db"), nil
|
return filepath.Join(dir, "nib.db"), nil
|
||||||
|
|||||||
+2
-1
@@ -1946,7 +1946,8 @@
|
|||||||
function renderMd(s) {
|
function renderMd(s) {
|
||||||
if (!s) return '';
|
if (!s) return '';
|
||||||
if (typeof marked === 'undefined') return escHtml(s);
|
if (typeof marked === 'undefined') return escHtml(s);
|
||||||
return marked.parse(s, { breaks: true });
|
const html = marked.parse(s, { breaks: true });
|
||||||
|
return typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : escHtml(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafeUrl(url) {
|
function isSafeUrl(url) {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
|
||||||
<script src="/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user