summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhaturatu <taro@eyes4you.org>2025-10-13 01:41:44 +0900
committerhaturatu <taro@eyes4you.org>2025-10-13 01:41:44 +0900
commitbb4fdc82353e3a0baef835bd0dce72c4eca74df0 (patch)
tree569f081aee65c0d9f078cb2dd012d75c403215ef
parentd758543bf215ff224dcb57f774180fa8e4279f9f (diff)
fix(api): api endpoint csrf
-rw-r--r--README.ja.md89
-rw-r--r--README.md89
-rw-r--r--cmd/server/main.go138
-rw-r--r--go.mod4
-rw-r--r--go.sum2
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
+ ```
+
## 設定詳細
| 環境変数 | 説明 | デフォルト値 |
diff --git a/README.md b/README.md
index e6a541f..84bd06d 100644
--- a/README.md
+++ b/README.md
@@ -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
+ }
diff --git a/go.mod b/go.mod
index 6375907..46efb6f 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index aabfbb8..78d85ff 100644
--- a/go.sum
+++ b/go.sum
@@ -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=