initial commit
This commit is contained in:
194
cmd/mock-oauth/main.go
Normal file
194
cmd/mock-oauth/main.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// mock-oauth is a minimal OAuth2 server for local development and testing.
|
||||
// It mimics Google/GitHub OAuth2 Authorization Code flow so you can test
|
||||
// login without real OAuth provider credentials.
|
||||
//
|
||||
// Flow:
|
||||
// 1. Frontend gets auth URL from your backend (POST /oauth/redirect-url with provider=mock)
|
||||
// 2. User is redirected to this mock's /authorize
|
||||
// 3. User clicks "Login" → mock redirects to your frontend's redirect_uri with ?code=...&state=...
|
||||
// 4. Frontend sends code to your backend (POST /oauth/callback)
|
||||
// 5. Backend exchanges code for token at this mock's /token, gets user at /userinfo
|
||||
//
|
||||
// Run: go run cmd/mock-oauth/main.go
|
||||
// Default: http://localhost:9999
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = "9999"
|
||||
mockCode = "mock_auth_code_12345"
|
||||
mockAccessToken = "mock_access_token_67890"
|
||||
mockEmail = "dev@example.com"
|
||||
mockName = "Dev User"
|
||||
mockGivenName = "Dev"
|
||||
mockFamilyName = "User"
|
||||
mockID = "mock-user-001"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := defaultPort
|
||||
if p := strings.TrimSpace(os.Getenv("PORT")); p != "" {
|
||||
port = p
|
||||
}
|
||||
|
||||
http.HandleFunc("/authorize", handleAuthorize)
|
||||
http.HandleFunc("/token", handleToken)
|
||||
http.HandleFunc("/userinfo", handleUserinfo)
|
||||
http.HandleFunc("/", handleRoot)
|
||||
|
||||
addr := ":" + port
|
||||
log.Printf("Mock OAuth server running at http://localhost%s", addr)
|
||||
log.Printf(" /authorize - OAuth2 authorize (redirect_uri, state, client_id)")
|
||||
log.Printf(" /token - OAuth2 token exchange")
|
||||
log.Printf(" /userinfo - User info (Bearer token)")
|
||||
log.Printf("")
|
||||
log.Printf("Configure your app: oauth.mock.base_url=http://localhost%s", addr)
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("Mock OAuth2 Server\n\nEndpoints: /authorize, /token, /userinfo"))
|
||||
}
|
||||
|
||||
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
_ = r.ParseForm()
|
||||
redirectURI := r.FormValue("redirect_uri")
|
||||
if redirectURI == "" {
|
||||
redirectURI = r.URL.Query().Get("redirect_uri")
|
||||
}
|
||||
state := r.FormValue("state")
|
||||
if state == "" {
|
||||
state = r.URL.Query().Get("state")
|
||||
}
|
||||
clientID := r.FormValue("client_id")
|
||||
if clientID == "" {
|
||||
clientID = r.URL.Query().Get("client_id")
|
||||
}
|
||||
|
||||
if redirectURI == "" {
|
||||
http.Error(w, "redirect_uri is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue("approve") != "" || r.FormValue("login") != "" {
|
||||
redir, _ := urlAddQuery(redirectURI, map[string]string{
|
||||
"code": mockCode,
|
||||
"state": state,
|
||||
})
|
||||
http.Redirect(w, r, redir, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Show simple login page
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
html := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Mock OAuth Login</title></head>
|
||||
<body style="font-family: sans-serif; max-width: 400px; margin: 60px auto; padding: 20px;">
|
||||
<h1>Mock OAuth2</h1>
|
||||
<p>Local development login. Client: %s</p>
|
||||
<form method="post" action="/authorize">
|
||||
<input type="hidden" name="redirect_uri" value="%s" />
|
||||
<input type="hidden" name="state" value="%s" />
|
||||
<input type="hidden" name="client_id" value="%s" />
|
||||
<button type="submit" name="approve" value="1" style="padding: 10px 24px; font-size: 16px;">Login as Dev User</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>`,
|
||||
escapeHTML(clientID),
|
||||
escapeHTML(redirectURI), escapeHTML(state), escapeHTML(clientID))
|
||||
_, _ = w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func urlAddQuery(base string, params map[string]string) (string, error) {
|
||||
u, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
q := u.Query()
|
||||
for k, v := range params {
|
||||
if v != "" {
|
||||
q.Set(k, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func escapeHTML(s string) string {
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, "\"", """)
|
||||
return s
|
||||
}
|
||||
|
||||
func handleToken(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
code := r.FormValue("code")
|
||||
grantType := r.FormValue("grant_type")
|
||||
if grantType != "authorization_code" || code != mockCode {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "invalid code or grant_type",
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": mockAccessToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "mock_refresh_token",
|
||||
})
|
||||
}
|
||||
|
||||
func handleUserinfo(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
http.Error(w, "missing or invalid Authorization", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
if token != mockAccessToken {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"id": mockID,
|
||||
"email": mockEmail,
|
||||
"name": mockName,
|
||||
"given_name": mockGivenName,
|
||||
"family_name": mockFamilyName,
|
||||
})
|
||||
}
|
||||
46
cmd/root.go
Normal file
46
cmd/root.go
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "base",
|
||||
Short: "A brief description of your application",
|
||||
Long: `A longer description that spans multiple lines and likely contains
|
||||
examples and usage of using your application. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.base.yaml)")
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
108
cmd/server.go
Normal file
108
cmd/server.go
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"base/internal/pkg"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/config"
|
||||
"base/internal/application"
|
||||
"base/internal/delivery"
|
||||
"base/internal/repository"
|
||||
"base/internal/server"
|
||||
"base/internal/server/middleware"
|
||||
"base/pkg/metrics"
|
||||
)
|
||||
|
||||
// serverCmd represents the server command
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "A brief description of your command",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("server called")
|
||||
serverInit()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
}
|
||||
|
||||
func serverInit() {
|
||||
app := fx.New(
|
||||
fx.Supply(metrics.GetMetrics("base", "api", "base-service")),
|
||||
fx.Provide(config.NewConfig),
|
||||
fx.Provide(middleware.NewMiddleware),
|
||||
pkg.Module,
|
||||
application.Module,
|
||||
repository.Module,
|
||||
delivery.Module,
|
||||
server.Server,
|
||||
fx.Invoke(registerHooks),
|
||||
)
|
||||
|
||||
startCtx, startCtxCancel := context.WithTimeout(context.Background(), fx.DefaultTimeout*10)
|
||||
defer startCtxCancel()
|
||||
|
||||
if err := app.Start(startCtx); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Wait for interrupt signal
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-c
|
||||
|
||||
stopCtx, stopCtxCancel := context.WithTimeout(context.Background(), fx.DefaultTimeout)
|
||||
defer stopCtxCancel()
|
||||
|
||||
if err := app.Stop(stopCtx); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// registerHooks registers lifecycle hooks with fx
|
||||
func registerHooks(
|
||||
lc fx.Lifecycle,
|
||||
l zerolog.Logger,
|
||||
c *config.AppConfig,
|
||||
m *metrics.Metrics,
|
||||
) error {
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
config.PrintConfig(l, c)
|
||||
|
||||
// Start system metrics collection
|
||||
if c.Metrics.Enabled {
|
||||
l.Info().Msg("System metrics collection started")
|
||||
}
|
||||
|
||||
l.Info().Msg("Application started")
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
l.Info().Msg("Shutting down application")
|
||||
l.Info().Msg("Application stopped")
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user