diff --git a/config.yml b/config.yml index 36e07403..b08cb4b5 100644 --- a/config.yml +++ b/config.yml @@ -5,9 +5,6 @@ log-level: warn # If not set, uses the URL from the request external-url: -# Prevents the creation of new accounts (either `true` or `false`). Default: false -disable-signup: false - # Directory where Opengist will store its data. Default: ~/.opengist/ opengist-home: diff --git a/internal/config/config.go b/internal/config/config.go index f776e616..a0588718 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,11 +18,10 @@ var C *config // Not using nested structs because the library // doesn't support dot notation in this case sadly type config struct { - LogLevel string `yaml:"log-level"` - ExternalUrl string `yaml:"external-url"` - DisableSignup bool `yaml:"disable-signup"` - OpengistHome string `yaml:"opengist-home"` - DBFilename string `yaml:"db-filename"` + LogLevel string `yaml:"log-level"` + ExternalUrl string `yaml:"external-url"` + OpengistHome string `yaml:"opengist-home"` + DBFilename string `yaml:"db-filename"` HttpHost string `yaml:"http.host"` HttpPort string `yaml:"http.port"` @@ -46,7 +45,6 @@ func configWithDefaults() (*config, error) { } c.LogLevel = "warn" - c.DisableSignup = false c.OpengistHome = filepath.Join(homeDir, ".opengist") c.DBFilename = "opengist.db" diff --git a/internal/models/admin_setting.go b/internal/models/admin_setting.go new file mode 100644 index 00000000..5fa7f6eb --- /dev/null +++ b/internal/models/admin_setting.go @@ -0,0 +1,56 @@ +package models + +import ( + "errors" + "github.com/mattn/go-sqlite3" + "gorm.io/gorm/clause" +) + +type AdminSetting struct { + Key string `gorm:"uniqueIndex"` + Value string +} + +const ( + SettingDisableSignup = "disable-signup" +) + +func GetSetting(key string) (string, error) { + var setting AdminSetting + err := db.Where("key = ?", key).First(&setting).Error + return setting.Value, err +} + +func UpdateSetting(key string, value string) error { + return db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "key"}}, // key colume + DoUpdates: clause.AssignmentColumns([]string{"value"}), + }).Create(&AdminSetting{ + Key: key, + Value: value, + }).Error +} + +func setSetting(key string, value string) error { + return db.Create(&AdminSetting{Key: key, Value: value}).Error +} + +func initAdminSettings(settings map[string]string) error { + for key, value := range settings { + if err := setSetting(key, value); err != nil { + if !isUniqueConstraintViolation(err) { + return err + } + } + } + + return nil +} + +func isUniqueConstraintViolation(err error) bool { + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { + return true + } + return false +} diff --git a/internal/models/db.go b/internal/models/db.go index c6d76bfc..68780557 100644 --- a/internal/models/db.go +++ b/internal/models/db.go @@ -17,11 +17,14 @@ func Setup(dbpath string) error { return err } - if err = db.AutoMigrate(&User{}, &SSHKey{}, &Gist{}); err != nil { + if err = db.AutoMigrate(&User{}, &SSHKey{}, &Gist{}, &AdminSetting{}); err != nil { return err } - return nil + // Default admin setting values + return initAdminSettings(map[string]string{ + SettingDisableSignup: "0", + }) } func CountAll(table interface{}) (int64, error) { diff --git a/internal/web/admin.go b/internal/web/admin.go index c6375115..88b62b06 100644 --- a/internal/web/admin.go +++ b/internal/web/admin.go @@ -64,7 +64,7 @@ func adminUsers(ctx echo.Context) error { return errorRes(500, "Cannot get users", err) } - if err = paginate(ctx, data, pageInt, 10, "data", "admin/users", 1); err != nil { + if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil { return errorRes(404, "Page not found", nil) } @@ -82,7 +82,7 @@ func adminGists(ctx echo.Context) error { return errorRes(500, "Cannot get gists", err) } - if err = paginate(ctx, data, pageInt, 10, "data", "admin/gists", 1); err != nil { + if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil { return errorRes(404, "Page not found", nil) } @@ -101,7 +101,7 @@ func adminUserDelete(ctx echo.Context) error { } addFlash(ctx, "User has been deleted", "success") - return redirect(ctx, "/admin/users") + return redirect(ctx, "/admin-panel/users") } func adminGistDelete(ctx echo.Context) error { @@ -119,7 +119,7 @@ func adminGistDelete(ctx echo.Context) error { } addFlash(ctx, "Gist has been deleted", "success") - return redirect(ctx, "/admin/gists") + return redirect(ctx, "/admin-panel/gists") } func adminSyncReposFromFS(ctx echo.Context) error { @@ -148,7 +148,7 @@ func adminSyncReposFromFS(ctx echo.Context) error { } syncReposFromFS = false }() - return redirect(ctx, "/admin") + return redirect(ctx, "/admin-panel") } func adminSyncReposFromDB(ctx echo.Context) error { @@ -180,5 +180,18 @@ func adminSyncReposFromDB(ctx echo.Context) error { syncReposFromDB = false return }() - return redirect(ctx, "/admin") + return redirect(ctx, "/admin-panel") +} + +func adminSetSetting(ctx echo.Context) error { + key := ctx.FormValue("key") + value := ctx.FormValue("value") + + if err := models.UpdateSetting(key, value); err != nil { + return errorRes(500, "Cannot set setting", err) + } + + return ctx.JSON(200, map[string]interface{}{ + "success": true, + }) } diff --git a/internal/web/auth.go b/internal/web/auth.go index d2840243..a35b6dee 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -5,7 +5,6 @@ import ( "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "gorm.io/gorm" - "opengist/internal/config" "opengist/internal/models" ) @@ -16,7 +15,7 @@ func register(ctx echo.Context) error { } func processRegister(ctx echo.Context) error { - if config.C.DisableSignup { + if getData(ctx, "signupDisabled") == true { return errorRes(403, "Signing up is disabled", nil) } diff --git a/internal/web/run.go b/internal/web/run.go index 1d66e645..632ace6f 100644 --- a/internal/web/run.go +++ b/internal/web/run.go @@ -173,7 +173,7 @@ func Start() { g1.POST("/settings/ssh-keys", sshKeysProcess, logged) g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged) - g2 := g1.Group("/admin") + g2 := g1.Group("/admin-panel") { g2.Use(adminPermission) g2.GET("", adminIndex) @@ -183,6 +183,7 @@ func Start() { g2.POST("/gists/:gist/delete", adminGistDelete) g2.POST("/sync-fs", adminSyncReposFromFS) g2.POST("/sync-db", adminSyncReposFromDB) + g2.PUT("/set-setting", adminSetSetting) } g1.GET("/all", allGists) @@ -236,7 +237,12 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc { ctxValue := context.WithValue(ctx.Request().Context(), "data", echo.Map{}) ctx.SetRequest(ctx.Request().WithContext(ctxValue)) setData(ctx, "loadStartTime", time.Now()) - setData(ctx, "signupDisabled", config.C.DisableSignup) + + disableSignup, err := models.GetSetting(models.SettingDisableSignup) + if err != nil { + return errorRes(500, "Cannot read setting from database", err) + } + setData(ctx, "signupDisabled", disableSignup == "1") return next(ctx) } diff --git a/internal/web/util.go b/internal/web/util.go index d78a89d8..714f9594 100644 --- a/internal/web/util.go +++ b/internal/web/util.go @@ -148,7 +148,7 @@ func validateReservedKeywords(fl validator.FieldLevel) bool { name := fl.Field().String() restrictedNames := map[string]struct{}{} - for _, restrictedName := range []string{"assets", "register", "login", "logout", "config", "admin", "all"} { + for _, restrictedName := range []string{"assets", "register", "login", "logout", "config", "admin-panel", "all"} { restrictedNames[restrictedName] = struct{}{} } diff --git a/public/admin.ts b/public/admin.ts new file mode 100644 index 00000000..635ea42a --- /dev/null +++ b/public/admin.ts @@ -0,0 +1,22 @@ +document.addEventListener('DOMContentLoaded', () => { + registerDomSetting(document.getElementById('disable-signup') as HTMLInputElement); +}); + +const setSetting = (key: string, value: string) => { + const data = new URLSearchParams(); + data.append('key', key); + data.append('value', value); + data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value)); + fetch('/admin-panel/set-setting', { + method: 'PUT', + credentials: 'same-origin', + body: data, + }); +}; + +const registerDomSetting = (el: HTMLInputElement) => { + el.addEventListener('change', () => { + setSetting(el.id, el.checked ? '1' : '0'); + }); +}; + diff --git a/templates/base/admin_header.html b/templates/base/admin_header.html index d7be0b27..598a7cc0 100644 --- a/templates/base/admin_header.html +++ b/templates/base/admin_header.html @@ -9,11 +9,11 @@

Admin panel

diff --git a/templates/base/base_header.html b/templates/base/base_header.html index 7abf7d53..47de0e86 100644 --- a/templates/base/base_header.html +++ b/templates/base/base_header.html @@ -53,7 +53,7 @@
{{ if .userLogged }} {{ if .userLogged.IsAdmin }} - + {{ end }} @@ -91,7 +91,7 @@ Settings {{ if .userLogged.IsAdmin }} - Admin + Admin {{ end }} {{ end }}
diff --git a/templates/pages/admin_gists.html b/templates/pages/admin_gists.html index 6750e049..9cfa5d5b 100644 --- a/templates/pages/admin_gists.html +++ b/templates/pages/admin_gists.html @@ -28,7 +28,7 @@ {{ $gist.NbLikes }} {{ $gist.CreatedAt }} -
+ {{ $.csrfHtml }}
diff --git a/templates/pages/admin_index.html b/templates/pages/admin_index.html index 03fe60d6..9ecbd472 100644 --- a/templates/pages/admin_index.html +++ b/templates/pages/admin_index.html @@ -56,13 +56,13 @@ Actions
-
+ {{ .csrfHtml }}
-
+ {{ .csrfHtml }}
+ +
+
+
+ Settings +
+ {{ .csrfHtml }} +
+
+ + +
+
+
+
+ + {{ template "admin_footer" .}} {{ template "footer" .}} diff --git a/templates/pages/admin_users.html b/templates/pages/admin_users.html index 24760b8a..14354652 100644 --- a/templates/pages/admin_users.html +++ b/templates/pages/admin_users.html @@ -20,7 +20,7 @@ {{ $user.Username }} {{ $user.CreatedAt }} - + {{ $.csrfHtml }} diff --git a/vite.config.js b/vite.config.js index 9680146f..454e890f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,7 +9,7 @@ export default defineConfig({ assetsDir: 'assets', manifest: true, rollupOptions: { - input: ['./public/main.ts', './public/editor.ts'] + input: ['./public/main.ts', './public/editor.ts', './public/admin.ts'] } } -}) \ No newline at end of file +})