-
Notifications
You must be signed in to change notification settings - Fork 0
fix(examples): harden runnable example flows #770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,24 @@ | ||
| ROOT := ../.. | ||
| CONFIG := examples/flagship/gowdk.config.go | ||
| APP_DIR := examples/flagship/.gowdk/app | ||
| CONFIG := gowdk.config.go | ||
| APP_DIR := .gowdk/app | ||
| HOOK_FILE := $(APP_DIR)/gowdkapp/flagship_hooks.go | ||
|
|
||
| .PHONY: check routes build serve clean | ||
|
|
||
| check: | ||
| cd $(ROOT) && go run ./cmd/gowdk check --config $(CONFIG) | ||
| go run ../../cmd/gowdk check --config $(CONFIG) | ||
|
|
||
| routes: | ||
| cd $(ROOT) && go run ./cmd/gowdk routes --config $(CONFIG) | ||
| go run ../../cmd/gowdk routes --config $(CONFIG) | ||
|
|
||
| $(HOOK_FILE): apphooks/flagship_hooks.go.txt | ||
| mkdir -p $(ROOT)/$(APP_DIR)/gowdkapp | ||
| cp apphooks/flagship_hooks.go.txt $(ROOT)/$(HOOK_FILE) | ||
| mkdir -p $(APP_DIR)/gowdkapp | ||
| cp apphooks/flagship_hooks.go.txt $(HOOK_FILE) | ||
|
|
||
| build: $(HOOK_FILE) | ||
| cd $(ROOT) && go run ./cmd/gowdk build --config $(CONFIG) --target flagship | ||
| go run ../../cmd/gowdk build --config $(CONFIG) --target flagship | ||
|
|
||
| serve: build | ||
| GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b GOWDK_ADDR=127.0.0.1:8092 bin/flagship | ||
| GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b GOWDK_FLAGSHIP_SECRET=development-flagship-session-secret-32b GOWDK_FLAGSHIP_PASSWORD=demo-password GOWDK_ADDR=127.0.0.1:8092 bin/flagship | ||
|
|
||
| clean: | ||
| rm -rf .gowdk bin dist |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,10 +24,17 @@ import ( | |
| const sessionCookie = "gowdk_flagship_session" | ||
|
|
||
| func Login(_ context.Context, values form.Values) (response.Response, error) { | ||
| if len(sessionSecret()) == 0 { | ||
| return response.RedirectTo("/?login=failed"), nil | ||
|
Comment on lines
+27
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With this new secret requirement, the run command still shown in Useful? React with 👍 / 👎. |
||
| } | ||
| wantEmail, wantPassword, ok := configuredCredentials() | ||
| if !ok { | ||
| return response.RedirectTo("/?login=failed"), nil | ||
| } | ||
|
|
||
| email := strings.TrimSpace(values.First("email")) | ||
| password := values.First("password") | ||
| if !constantEqual(email, env("GOWDK_FLAGSHIP_EMAIL", "demo@example.com")) || | ||
| !constantEqual(password, env("GOWDK_FLAGSHIP_PASSWORD", "demo-password")) { | ||
| if !constantEqual(email, wantEmail) || !constantEqual(password, wantPassword) { | ||
| return response.RedirectTo("/?login=failed"), nil | ||
| } | ||
|
|
||
|
|
@@ -127,6 +134,9 @@ var sessions = struct { | |
| }{Values: map[string]session{}} | ||
|
|
||
| func currentSession(request *http.Request) (session, bool) { | ||
| if len(sessionSecret()) == 0 { | ||
| return session{}, false | ||
| } | ||
| if request == nil { | ||
| return session{}, false | ||
| } | ||
|
|
@@ -159,11 +169,21 @@ func sign(value string) string { | |
| } | ||
|
|
||
| func signature(value string) string { | ||
| mac := hmac.New(sha256.New, []byte(env("GOWDK_FLAGSHIP_SECRET", "development-flagship-secret-change-me"))) | ||
| mac := hmac.New(sha256.New, sessionSecret()) | ||
| _, _ = mac.Write([]byte(value)) | ||
| return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) | ||
| } | ||
|
|
||
| func sessionSecret() []byte { | ||
| return []byte(strings.TrimSpace(os.Getenv("GOWDK_FLAGSHIP_SECRET"))) | ||
| } | ||
|
|
||
| func configuredCredentials() (email, password string, ok bool) { | ||
| email = env("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") | ||
| password = strings.TrimSpace(os.Getenv("GOWDK_FLAGSHIP_PASSWORD")) | ||
| return email, password, password != "" | ||
| } | ||
|
|
||
| func sessionDuration() time.Duration { | ||
| return 12 * time.Hour | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package flagship | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/cssbruno/gowdk/runtime/form" | ||
| "github.com/cssbruno/gowdk/runtime/response" | ||
| ) | ||
|
|
||
| func TestLoginRequiresExplicitSessionSecret(t *testing.T) { | ||
| resetTestSessions(t) | ||
| t.Setenv("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") | ||
| t.Setenv("GOWDK_FLAGSHIP_SECRET", "") | ||
| t.Setenv("GOWDK_FLAGSHIP_PASSWORD", "demo-password") | ||
|
|
||
| result := loginForTest(t, "demo@example.com", "demo-password") | ||
|
|
||
| assertLoginFailed(t, result) | ||
| } | ||
|
|
||
| func TestLoginRequiresExplicitPassword(t *testing.T) { | ||
| resetTestSessions(t) | ||
| t.Setenv("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") | ||
| t.Setenv("GOWDK_FLAGSHIP_SECRET", "development-flagship-session-secret-32b") | ||
| t.Setenv("GOWDK_FLAGSHIP_PASSWORD", "") | ||
|
|
||
| result := loginForTest(t, "demo@example.com", "demo-password") | ||
|
|
||
| assertLoginFailed(t, result) | ||
| } | ||
|
|
||
| func TestLoginCreatesSignedSessionWithExplicitCredentials(t *testing.T) { | ||
| resetTestSessions(t) | ||
| t.Setenv("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") | ||
| t.Setenv("GOWDK_FLAGSHIP_SECRET", "development-flagship-session-secret-32b") | ||
| t.Setenv("GOWDK_FLAGSHIP_PASSWORD", "demo-password") | ||
|
|
||
| result := loginForTest(t, "demo@example.com", "demo-password") | ||
|
|
||
| if result.Kind != response.Redirect || result.URL != "/dashboard" { | ||
| t.Fatalf("login result = %#v, want redirect to dashboard", result) | ||
| } | ||
| if len(result.Cookies) != 1 { | ||
| t.Fatalf("cookies = %#v, want one session cookie", result.Cookies) | ||
| } | ||
| cookie := result.Cookies[0] | ||
| if cookie.Name != sessionCookie || !cookie.HttpOnly || cookie.Value == "" || !strings.Contains(cookie.Value, ".") { | ||
| t.Fatalf("session cookie = %#v", cookie) | ||
| } | ||
|
|
||
| request, err := http.NewRequest(http.MethodGet, "/dashboard", nil) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| request.AddCookie(&cookie) | ||
| if current, ok := currentSession(request); !ok || current.Email != "demo@example.com" { | ||
| t.Fatalf("current session = %#v ok=%v", current, ok) | ||
| } | ||
| } | ||
|
|
||
| func loginForTest(t *testing.T, email string, password string) response.Response { | ||
| t.Helper() | ||
| result, err := Login(context.Background(), form.Values{ | ||
| "email": {email}, | ||
| "password": {password}, | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("Login returned error: %v", err) | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| func assertLoginFailed(t *testing.T, result response.Response) { | ||
| t.Helper() | ||
| if result.Kind != response.Redirect || result.URL != "/?login=failed" { | ||
| t.Fatalf("login result = %#v, want failed redirect", result) | ||
| } | ||
| if len(result.Cookies) != 0 { | ||
| t.Fatalf("failed login set cookies: %#v", result.Cookies) | ||
| } | ||
| } | ||
|
|
||
| func resetTestSessions(t *testing.T) { | ||
| t.Helper() | ||
| sessions.Lock() | ||
| defer sessions.Unlock() | ||
| sessions.Values = map[string]session{} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a reader has
tailwindcssinstalled and runs this newly documented root command, the project helper executes the build fromexamples/tailwind, while the config still sets the Tailwind input toexamples/tailwind/app.css. The generated Tailwind input therefore importsexamples/tailwind/examples/tailwind/app.css, which does not exist, so the build fails before producing the example output; either make the config input project-relative or document a command that matches the existing config.Useful? React with 👍 / 👎.