44 "encoding/json"
55 "net/http"
66 "sync"
7+ "time"
78
89 "simpleauth/internal/store"
910)
@@ -23,9 +24,14 @@ func (h *Handler) initMigrationState() {
2324// handleDatabaseInfo returns info about the current database backend.
2425// GET /api/admin/database/info
2526func (h * Handler ) handleDatabaseInfo (w http.ResponseWriter , r * http.Request ) {
27+ dbCfg , _ := store .LoadDBConfig (h .cfg .DataDir )
2628 info := map [string ]interface {}{
2729 "backend" : h .storeBackend (),
2830 }
31+ if dbCfg != nil && dbCfg .PostgresURL != "" {
32+ // Mask password in URL for display
33+ info ["postgres_configured" ] = true
34+ }
2935 jsonResp (w , info , http .StatusOK )
3036}
3137
@@ -62,21 +68,19 @@ func (h *Handler) handleMigrateTest(w http.ResponseWriter, r *http.Request) {
6268 jsonResp (w , map [string ]interface {}{"ok" : true }, http .StatusOK )
6369}
6470
65- // handleMigrateStart starts a BoltDB → Postgres migration .
71+ // handleMigrateStart starts a migration (BoltDB→ Postgres or Postgres→BoltDB) .
6672// POST /api/admin/database/migrate
6773func (h * Handler ) handleMigrateStart (w http.ResponseWriter , r * http.Request ) {
6874 var req struct {
69- PostgresURL string `json:"postgres_url"`
75+ PostgresURL string `json:"postgres_url"` // required for bolt→pg
76+ Direction string `json:"direction"` // "to_postgres" (default) or "to_boltdb"
7077 }
71- if err := readJSON (r , & req ); err != nil || req . PostgresURL == "" {
72- jsonError (w , "postgres_url is required " , http .StatusBadRequest )
78+ if err := readJSON (r , & req ); err != nil {
79+ jsonError (w , "invalid request body " , http .StatusBadRequest )
7380 return
7481 }
75-
76- boltStore , ok := h .store .(* store.BoltStore )
77- if ! ok {
78- jsonError (w , "migration only supported from BoltDB backend" , http .StatusBadRequest )
79- return
82+ if req .Direction == "" {
83+ req .Direction = "to_postgres"
8084 }
8185
8286 h .migration .mu .RLock ()
@@ -87,25 +91,15 @@ func (h *Handler) handleMigrateStart(w http.ResponseWriter, r *http.Request) {
8791 }
8892 h .migration .mu .RUnlock ()
8993
90- // Open target Postgres
91- target , err := store .OpenPostgres (req .PostgresURL )
92- if err != nil {
93- jsonError (w , "failed to connect to postgres: " + err .Error (), http .StatusBadRequest )
94- return
95- }
96-
9794 // Reset status
9895 h .migration .mu .Lock ()
9996 h .migration .status = store.MigrationStatus {State : "running" , Progress : map [string ]string {}}
10097 h .migration .mu .Unlock ()
10198
10299 statusCh := make (chan store.MigrationStatus , 100 )
103100
104- // Background migration
105- go func () {
106- defer target .Close ()
107-
108- // Consume status updates
101+ // Consume status updates in background
102+ consumeStatus := func () chan struct {} {
109103 done := make (chan struct {})
110104 go func () {
111105 for s := range statusCh {
@@ -115,22 +109,80 @@ func (h *Handler) handleMigrateStart(w http.ResponseWriter, r *http.Request) {
115109 }
116110 close (done )
117111 }()
112+ return done
113+ }
118114
119- err := store .MigrateToPostgres (boltStore , target , statusCh )
120- close (statusCh )
121- <- done
115+ switch req .Direction {
116+ case "to_postgres" :
117+ if req .PostgresURL == "" {
118+ jsonError (w , "postgres_url is required" , http .StatusBadRequest )
119+ return
120+ }
121+ boltStore , ok := h .store .(* store.BoltStore )
122+ if ! ok {
123+ jsonError (w , "current backend is not BoltDB" , http .StatusBadRequest )
124+ return
125+ }
126+ target , err := store .OpenPostgres (req .PostgresURL )
127+ if err != nil {
128+ jsonError (w , "failed to connect to postgres: " + err .Error (), http .StatusBadRequest )
129+ return
130+ }
122131
132+ go func () {
133+ done := consumeStatus ()
134+ err := store .MigrateToPostgres (boltStore , target , statusCh )
135+ close (statusCh )
136+ <- done
137+ target .Close ()
138+
139+ if err != nil {
140+ h .migration .mu .Lock ()
141+ h .migration .status .State = "failed"
142+ h .migration .status .Error = err .Error ()
143+ h .migration .mu .Unlock ()
144+ }
145+ }()
146+
147+ h .audit ("migration_started" , "admin" , getClientIP (r ), map [string ]interface {}{
148+ "direction" : "to_postgres" ,
149+ })
150+
151+ case "to_boltdb" :
152+ pgStore , ok := h .store .(* store.PostgresStore )
153+ if ! ok {
154+ jsonError (w , "current backend is not PostgreSQL" , http .StatusBadRequest )
155+ return
156+ }
157+ target , err := store .OpenBolt (h .cfg .DataDir )
123158 if err != nil {
124- h .migration .mu .Lock ()
125- h .migration .status .State = "failed"
126- h .migration .status .Error = err .Error ()
127- h .migration .mu .Unlock ()
159+ jsonError (w , "failed to open BoltDB: " + err .Error (), http .StatusInternalServerError )
160+ return
128161 }
129- }()
130162
131- h .audit ("migration_started" , "admin" , getClientIP (r ), map [string ]interface {}{
132- "target" : "postgres" ,
133- })
163+ go func () {
164+ done := consumeStatus ()
165+ err := store .MigrateFromPostgres (pgStore , target , statusCh )
166+ close (statusCh )
167+ <- done
168+ target .Close ()
169+
170+ if err != nil {
171+ h .migration .mu .Lock ()
172+ h .migration .status .State = "failed"
173+ h .migration .status .Error = err .Error ()
174+ h .migration .mu .Unlock ()
175+ }
176+ }()
177+
178+ h .audit ("migration_started" , "admin" , getClientIP (r ), map [string ]interface {}{
179+ "direction" : "to_boltdb" ,
180+ })
181+
182+ default :
183+ jsonError (w , "direction must be 'to_postgres' or 'to_boltdb'" , http .StatusBadRequest )
184+ return
185+ }
134186
135187 jsonResp (w , map [string ]string {"status" : "started" }, http .StatusAccepted )
136188}
@@ -146,3 +198,57 @@ func (h *Handler) handleMigrateStatus(w http.ResponseWriter, r *http.Request) {
146198 w .Header ().Set ("Content-Type" , "application/json" )
147199 w .Write (data )
148200}
201+
202+ // handleSwitchBackend saves the backend choice to db.json and triggers restart.
203+ // POST /api/admin/database/switch
204+ func (h * Handler ) handleSwitchBackend (w http.ResponseWriter , r * http.Request ) {
205+ var req struct {
206+ Backend string `json:"backend"` // "boltdb" or "postgres"
207+ PostgresURL string `json:"postgres_url"` // required when backend=postgres
208+ }
209+ if err := readJSON (r , & req ); err != nil {
210+ jsonError (w , "invalid request body" , http .StatusBadRequest )
211+ return
212+ }
213+
214+ switch req .Backend {
215+ case "boltdb" :
216+ if err := store .SaveDBConfig (h .cfg .DataDir , & store.DBConfig {Backend : "boltdb" }); err != nil {
217+ jsonError (w , "failed to save config: " + err .Error (), http .StatusInternalServerError )
218+ return
219+ }
220+ case "postgres" :
221+ if req .PostgresURL == "" {
222+ jsonError (w , "postgres_url is required" , http .StatusBadRequest )
223+ return
224+ }
225+ if err := store .TestPostgresConnection (req .PostgresURL ); err != nil {
226+ jsonError (w , "postgres connection failed: " + err .Error (), http .StatusBadRequest )
227+ return
228+ }
229+ if err := store .SaveDBConfig (h .cfg .DataDir , & store.DBConfig {Backend : "postgres" , PostgresURL : req .PostgresURL }); err != nil {
230+ jsonError (w , "failed to save config: " + err .Error (), http .StatusInternalServerError )
231+ return
232+ }
233+ default :
234+ jsonError (w , "backend must be 'boltdb' or 'postgres'" , http .StatusBadRequest )
235+ return
236+ }
237+
238+ h .audit ("backend_switched" , "admin" , getClientIP (r ), map [string ]interface {}{
239+ "backend" : req .Backend ,
240+ })
241+
242+ jsonResp (w , map [string ]string {"status" : "saved" , "backend" : req .Backend }, http .StatusOK )
243+
244+ // Trigger restart if channel available
245+ if h .restartCh != nil {
246+ go func () {
247+ time .Sleep (200 * time .Millisecond )
248+ select {
249+ case h .restartCh <- struct {}{}:
250+ default :
251+ }
252+ }()
253+ }
254+ }
0 commit comments