diff options
| -rw-r--r-- | README.ja.md | 89 | ||||
| -rw-r--r-- | README.md | 89 | ||||
| -rw-r--r-- | cmd/server/main.go | 138 | ||||
| -rw-r--r-- | go.mod | 4 | ||||
| -rw-r--r-- | go.sum | 2 |
5 files changed, 244 insertions, 78 deletions
diff --git a/README.ja.md b/README.ja.md index 9538f04..d76d5d6 100644 --- a/README.ja.md +++ b/README.ja.md @@ -343,6 +343,95 @@ Authorization: Bearer <access_token> **注意:** トークンの有効期間は `.env` ファイルで設定できます。詳細は設定表を参照してください。 +## 管理APIエンドポイント + +管理タスクのために、プロキシはプログラムからアクセス可能なAPIエンドポイントをいくつか提供しています。これらのエンドポイントはすべて、`admin`ロールを持つユーザーの有効なベアラートークンが必要です。 + +これらのエンドポイントのベースパスは `/api/admin/users` であり、これは `AUTH_PATH_ADMIN_USERS_API` 環境変数でカスタマイズ可能です。 + +### ユーザー一覧の取得 + +- **エンドポイント**: `GET /api/admin/users` +- **説明**: システム内の全ユーザーのリストを取得します。 +- **例**: + ```sh + ACCESS_TOKEN="your_admin_access_token" + curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:8080/api/admin/users + ``` + +### ユーザーの作成 + +- **エンドポイント**: `POST /api/admin/users` +- **説明**: 新しいユーザーを作成します。 +- **ボディ**: + ```json + { + "username": "newuser", + "password": "a-strong-password", + "role": "user" + } + ``` +- **例**: + ```sh + ACCESS_TOKEN="your_admin_access_token" + curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"username": "newuser", "password": "Password123", "role": "user"}' \ + http://localhost:8080/api/admin/users + ``` + +### ユーザーロールの更新 + +- **エンドポイント**: `POST /api/admin/users/{id}/role` +- **説明**: IDで指定された特定のユーザーのロールを更新します。 +- **ボディ**: + ```json + { + "role": "admin" + } + ``` +- **例**: + ```sh + USER_ID=2 + ACCESS_TOKEN="your_admin_access_token" + curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"role": "admin"}' \ + http://localhost:8080/api/admin/users/$USER_ID/role + ``` + +### ユーザーステータスの更新 + +- **エンドポイント**: `POST /api/admin/users/{id}/status` +- **説明**: IDで指定されたユーザーを有効化または無効化します。 +- **ボディ**: + ```json + { + "is_active": false + } + ``` +- **例**: + ```sh + USER_ID=2 + ACCESS_TOKEN="your_admin_access_token" + curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"is_active": false}' \ + http://localhost:8080/api/admin/users/$USER_ID/status + ``` + +### ユーザーの削除 + +- **エンドポイント**: `DELETE /api/admin/users/{id}` +- **説明**: IDで指定されたユーザーを削除します。 +- **例**: + ```sh + USER_ID=2 + ACCESS_TOKEN="your_admin_access_token" + curl -X DELETE -H "Authorization: Bearer $ACCESS_TOKEN" \ + http://localhost:8080/api/admin/users/$USER_ID + ``` + ## 設定詳細 | 環境変数 | 説明 | デフォルト値 | @@ -341,6 +341,95 @@ The `refresh_token` is a long-lived token that can be used to obtain a new acces **Note:** The token lifetimes can be configured in your `.env` file. See the configuration table for details. +## Admin API Endpoints + +For administrative tasks, the proxy provides several API endpoints that can be accessed programmatically. All of these endpoints require a valid Bearer Token from a user with the `admin` role. + +The base path for these endpoints is `/api/admin/users`, which can be customized with the `AUTH_PATH_ADMIN_USERS_API` environment variable. + +### List Users + +- **Endpoint**: `GET /api/admin/users` +- **Description**: Retrieves a list of all users in the system. +- **Example**: + ```sh + ACCESS_TOKEN="your_admin_access_token" + curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:8080/api/admin/users + ``` + +### Create User + +- **Endpoint**: `POST /api/admin/users` +- **Description**: Creates a new user. +- **Body**: + ```json + { + "username": "newuser", + "password": "a-strong-password", + "role": "user" + } + ``` +- **Example**: + ```sh + ACCESS_TOKEN="your_admin_access_token" + curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"username": "newuser", "password": "Password123", "role": "user"}' \ + http://localhost:8080/api/admin/users + ``` + +### Update User Role + +- **Endpoint**: `POST /api/admin/users/{id}/role` +- **Description**: Updates the role of a specific user by their ID. +- **Body**: + ```json + { + "role": "admin" + } + ``` +- **Example**: + ```sh + USER_ID=2 + ACCESS_TOKEN="your_admin_access_token" + curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"role": "admin"}' \ + http://localhost:8080/api/admin/users/$USER_ID/role + ``` + +### Update User Status + +- **Endpoint**: `POST /api/admin/users/{id}/status` +- **Description**: Activates or deactivates a user by their ID. +- **Body**: + ```json + { + "is_active": false + } + ``` +- **Example**: + ```sh + USER_ID=2 + ACCESS_TOKEN="your_admin_access_token" + curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"is_active": false}' \ + http://localhost:8080/api/admin/users/$USER_ID/status + ``` + +### Delete User + +- **Endpoint**: `DELETE /api/admin/users/{id}` +- **Description**: Deletes a user by their ID. +- **Example**: + ```sh + USER_ID=2 + ACCESS_TOKEN="your_admin_access_token" + curl -X DELETE -H "Authorization: Bearer $ACCESS_TOKEN" \ + http://localhost:8080/api/admin/users/$USER_ID + ``` + ## Configuration Details | Environment Variable | Description | Default Value | diff --git a/cmd/server/main.go b/cmd/server/main.go index b89362b..36c367c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -46,16 +46,16 @@ func main() { os.Exit(1) } - // --- Create router and define routes --- - mux := http.NewServeMux() + // --- Create routers for Web and API --- + webMux := http.NewServeMux() + apiMux := http.NewServeMux() - // --- Static file serving --- + // --- Static file serving (Web) --- assetsPath := strings.TrimRight(config.Paths.Assets, "/") + "/" assetsFS := http.FileServer(http.Dir("templates")) - mux.Handle(assetsPath, http.StripPrefix(assetsPath, assetsFS)) + webMux.Handle(assetsPath, http.StripPrefix(assetsPath, assetsFS)) - // --- Frontend selection and routing --- - frontendType := os.Getenv("FRONTEND_TYPE") + // --- Rate Limiter --- rateLimiter := middleware.NewRateLimiter() // Helper to switch handler based on HTTP method @@ -69,50 +69,45 @@ func main() { }) } - if frontendType == "php" { - // Create specific handlers for each PHP page + // --- Web UI Routes (CSRF-protected) --- + if frontendType := os.Getenv("FRONTEND_TYPE"); frontendType == "php" { + // PHP frontend routes loginPageHandler := handlers.NewPhpProxyHandler("login.php") registerPageHandler := handlers.NewPhpProxyHandler("register.php") accountPageHandler := handlers.NewPhpProxyHandler("account.php") accountPasswordPageHandler := handlers.NewPhpProxyHandler("account_password.php") - // For /login, GET goes to PHP, POST goes to Go's login logic loginHandler := methodSwitch(loginPageHandler, http.HandlerFunc(handlers.LoginHandler)) - mux.Handle(config.Paths.Login, loginHandler) + webMux.Handle(config.Paths.Login, loginHandler) if config.Paths.RegisterEnabled { - // For /register, GET goes to PHP, POST goes to Go's register logic registerPostHandler := middleware.RateLimitMiddleware(rateLimiter)(http.HandlerFunc(handlers.RegisterHandler)) registerHandler := methodSwitch(registerPageHandler, registerPostHandler) - mux.Handle(config.Paths.Register, registerHandler) + webMux.Handle(config.Paths.Register, registerHandler) } - // For account pages, GET goes to PHP, POST (for password change) goes to Go accountPasswordPostHandler := http.HandlerFunc(handlers.ChangePasswordHandler) accountPasswordHandler := methodSwitch(accountPasswordPageHandler, accountPasswordPostHandler) - mux.Handle(config.Paths.AccountPassword, middleware.SessionAuth(accountPasswordHandler)) - mux.Handle(config.Paths.Account, middleware.SessionAuth(accountPageHandler)) // This page is GET only - - // Admin page still uses Go templates - mux.Handle(config.Paths.Admin, middleware.SessionAuth(middleware.AdminMiddleware(http.HandlerFunc(handlers.AdminPageHandler)))) + webMux.Handle(config.Paths.AccountPassword, middleware.SessionAuth(accountPasswordHandler)) + webMux.Handle(config.Paths.Account, middleware.SessionAuth(accountPageHandler)) + webMux.Handle(config.Paths.Admin, middleware.SessionAuth(middleware.AdminMiddleware(http.HandlerFunc(handlers.AdminPageHandler)))) } else { - // --- Public Auth Routes (Go templates) --- - mux.HandleFunc(config.Paths.Login, handlers.LoginHandler) + // Go template frontend routes + webMux.HandleFunc(config.Paths.Login, handlers.LoginHandler) if config.Paths.RegisterEnabled { - mux.Handle(config.Paths.Register, middleware.RateLimitMiddleware(rateLimiter)(http.HandlerFunc(handlers.RegisterHandler))) + webMux.Handle(config.Paths.Register, middleware.RateLimitMiddleware(rateLimiter)(http.HandlerFunc(handlers.RegisterHandler))) } - // --- Auth HTML pages (Protected by SessionAuth, Go templates) --- - mux.Handle(config.Paths.Account, middleware.SessionAuth(http.HandlerFunc(handlers.AccountPageHandler))) - mux.Handle(config.Paths.Admin, middleware.SessionAuth(middleware.AdminMiddleware(http.HandlerFunc(handlers.AdminPageHandler)))) - mux.Handle(config.Paths.AccountPassword, middleware.SessionAuth(http.HandlerFunc(handlers.ChangePasswordHandler))) + webMux.Handle(config.Paths.Account, middleware.SessionAuth(http.HandlerFunc(handlers.AccountPageHandler))) + webMux.Handle(config.Paths.Admin, middleware.SessionAuth(middleware.AdminMiddleware(http.HandlerFunc(handlers.AdminPageHandler)))) + webMux.Handle(config.Paths.AccountPassword, middleware.SessionAuth(http.HandlerFunc(handlers.ChangePasswordHandler))) } + webMux.HandleFunc(config.Paths.Logout, handlers.LogoutHandler) - // --- Common Routes --- - mux.HandleFunc(config.Paths.Logout, handlers.LogoutHandler) - mux.HandleFunc("/auth/refresh", handlers.RefreshTokenHandler) - mux.HandleFunc("/api/auth/token", handlers.TokenHandler) + // --- API Routes (No CSRF) --- + apiMux.HandleFunc("/api/auth/token", handlers.TokenHandler) + apiMux.HandleFunc("/auth/refresh", handlers.RefreshTokenHandler) - // --- Protected API Endpoints (for Admin UI, protected by SessionAuth) --- + // Admin API endpoints adminCreateUserAPI := http.HandlerFunc(handlers.CreateUserHandler) getUsersHandler := http.HandlerFunc(handlers.GetUsersHandler) adminUsersAPI := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -122,13 +117,12 @@ func main() { getUsersHandler.ServeHTTP(w, r) } }) - adminUpdateRoleAPI := http.HandlerFunc(handlers.UpdateUserRoleHandler) adminDeleteUserAPI := http.HandlerFunc(handlers.DeleteUserHandler) adminSetStatusAPI := http.HandlerFunc(handlers.SetUserActiveStatusHandler) - mux.Handle(config.Paths.AdminUsersAPI, middleware.BearerAuth(middleware.AdminMiddleware(adminUsersAPI))) - mux.Handle(config.Paths.AdminUsersAPI+"/", middleware.BearerAuth(middleware.AdminMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + apiMux.Handle(config.Paths.AdminUsersAPI, middleware.BearerAuth(middleware.AdminMiddleware(adminUsersAPI))) + apiMux.Handle(config.Paths.AdminUsersAPI+"/", middleware.BearerAuth(middleware.AdminMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path switch { case strings.HasSuffix(path, "/role") && r.Method == http.MethodPost: @@ -145,51 +139,16 @@ func main() { // --- Main Application Proxy --- proxyHandler := handlers.NewProxy(targetURL) - finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, pattern := mux.Handler(r) - if pattern != "" { - mux.ServeHTTP(w, r) - return - } - - path := r.URL.Path - - // API Protection with Bearer Token - if config.Paths.ProtectAPI && strings.HasPrefix(path, config.Paths.APIPath) { - middleware.BearerAuth(proxyHandler).ServeHTTP(w, r) - return - } - - // Frontend Protection with Session Cookie - if config.Paths.ProtectFrontend { - middleware.SessionAuth(proxyHandler).ServeHTTP(w, r) - return - } - - // No protection - proxyHandler.ServeHTTP(w, r) - }) - - // Get server port - port := os.Getenv("LISTEN_PORT") - if port == "" { - port = "8080" - } - listenAddr := ":" + port - - // --- CSRF Protection --- + // --- CSRF Protection (for Web UI) --- csrfSecret := os.Getenv("CSRF_SECRET_KEY") if csrfSecret == "" { logging.AppLog.Error("CSRF_SECRET_KEY environment variable not set") os.Exit(1) } - // In production, you should set csrf.Secure(true). - // You can use an environment variable to control this setting. csrfOptions := []csrf.Option{ csrf.Secure(os.Getenv("ENV") == "production"), csrf.Path("/"), } - sameSiteMode := csrf.SameSiteLaxMode switch strings.ToLower(os.Getenv("CSRF_SAME_SITE")) { case "strict": @@ -198,17 +157,44 @@ func main() { sameSiteMode = csrf.SameSiteNoneMode } csrfOptions = append(csrfOptions, csrf.SameSite(sameSiteMode)) - trustedOrigins := os.Getenv("CSRF_TRUSTED_ORIGINS") if trustedOrigins != "" { origins := strings.Split(trustedOrigins, ",") csrfOptions = append(csrfOptions, csrf.TrustedOrigins(origins)) } + csrfMiddleware := csrf.Protect([]byte(csrfSecret), csrfOptions...) + csrfProtectedWebMux := middleware.HandleOptions(csrfMiddleware(webMux)) + + // --- Final Handler --- + finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logging.AppLog.Info("Final handler received request", "path", r.URL.Path) + if strings.HasPrefix(r.URL.Path, "/api/") { + logging.AppLog.Info("Routing to API mux") + apiMux.ServeHTTP(w, r) + return + } - csrfMiddleware := csrf.Protect( - []byte(csrfSecret), - csrfOptions..., - ) + // Check if the path is a web UI path + _, webPattern := webMux.Handler(r) + if webPattern != "" { + csrfProtectedWebMux.ServeHTTP(w, r) + return + } + + // Fallback to proxy + if config.Paths.ProtectFrontend { + middleware.SessionAuth(proxyHandler).ServeHTTP(w, r) + } else { + proxyHandler.ServeHTTP(w, r) + } + }) + + // Get server port + port := os.Getenv("LISTEN_PORT") + if port == "" { + port = "8080" + } + listenAddr := ":" + port // --- Start Server --- logging.AppLog.Info("Server starting on " + listenAddr) @@ -216,7 +202,7 @@ func main() { // Start periodic background tasks go startBackgroundTasks(pool, rateLimiter) - if err := http.ListenAndServe(listenAddr, middleware.HandleOptions(csrfMiddleware(finalHandler))); err != nil { + if err := http.ListenAndServe(listenAddr, finalHandler); err != nil { logging.AppLog.Error("Server failed to start", "error", err) os.Exit(1) } @@ -320,4 +306,4 @@ func main() { }) - }
\ No newline at end of file + } @@ -4,15 +4,15 @@ go 1.25.2 require ( github.com/go-sql-driver/mysql v1.9.3 + github.com/gorilla/csrf v1.7.3 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.32 + github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 golang.org/x/crypto v0.43.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/gorilla/csrf v1.7.3 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 // indirect ) @@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0= github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= |
