-
-
Save crhntr/934a51390c91c23c52c3ce3516b05cc4 to your computer and use it in GitHub Desktop.
Clean Go API example with access policies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// I saw this in a reddit post. I thought I would refactor it using a functional pattern. | |
// I don't think my refactor makes sense after noticing that this pattern is what chi suggests. | |
package main | |
import ( | |
"context" | |
"encoding/json" | |
"fmt" | |
"net/http" | |
"github.com/go-chi/chi" | |
) | |
type User struct { | |
ID string | |
} | |
type Todo struct { | |
UserID string | |
Task string | |
Done bool | |
} | |
var users = map[string]User{ | |
"1": { | |
ID: "1", | |
}, | |
"2": { | |
ID: "2", | |
}, | |
} | |
var todos = map[string]Todo{ | |
"1": { | |
UserID: "1", | |
Task: "hello", | |
Done: false, | |
}, | |
"2": { | |
UserID: "2", | |
Task: "hello", | |
Done: false, | |
}, | |
} | |
type handlerWithUser func(w http.ResponseWriter, r *http.Request, user User) | |
func withUser(hf handlerWithUser) http.HandlerFunc { | |
return func(w http.ResponseWriter, r *http.Request) { | |
// Get user id from url query: /todos?user=1 | |
id := r.URL.Query().Get("user") | |
// Find user in users | |
user, ok := users[id] | |
if !ok { | |
http.Error(w, "user not found", http.StatusNotFound) | |
return | |
} | |
hf(w, r, user) | |
} | |
} | |
type handlerWithUserAndTodo func(w http.ResponseWriter, r *http.Request, user User) | |
func withTodo(hf handlerWithUserAndTodo) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
// Get todo id from url: /todos/{todoID} | |
id := chi.URLParam(r, "todoID") | |
// Find todo in todos | |
todo, ok := todos[id] | |
if !ok { | |
// If not found: 404 | |
http.Error(w, "todo not found", http.StatusNotFound) | |
return | |
} | |
hf(w, r, user) | |
}) | |
} | |
func withTodos(h http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
// Add all todos to request context | |
ctx := context.WithValue(r.Context(), "todos", todos) | |
h.ServeHTTP(w, r.WithContext(ctx)) | |
}) | |
} | |
func canViewTodos(h http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
// Get user from request context | |
user, hasUser := r.Context().Value("user").(User) | |
fmt.Println(user, hasUser) | |
if !hasUser { | |
// If no user present, send 403 | |
http.Error(w, "403 forbidden", http.StatusForbidden) | |
return | |
} | |
h.ServeHTTP(w, r) | |
}) | |
} | |
func canUpdateTodo(h http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
// Get user from request context | |
user, hasUser := r.Context().Value("user").(User) | |
// Get todo from request context | |
todo, hasTodo := r.Context().Value("todo").(Todo) | |
fmt.Println(user, hasUser) | |
fmt.Println(todo, hasTodo) | |
if !hasUser || !hasTodo || todo.UserID != user.ID { | |
// If no user, no todo present or todo does not belong to user: 403 | |
http.Error(w, "403 forbidden", http.StatusForbidden) | |
return | |
} | |
h.ServeHTTP(w, r) | |
}) | |
} | |
func index(w http.ResponseWriter, r *http.Request) { | |
// Print all todos as json | |
todos, _ := r.Context().Value("todos").(map[string]Todo) | |
json.NewEncoder(w).Encode(todos) | |
} | |
func update(w http.ResponseWriter, r *http.Request) { | |
// Get todo from request context | |
todo, _ := r.Context().Value("todo").(Todo) | |
// Manipulate todo | |
todo.Done = true | |
// Save todo | |
// Send updated todo as json | |
json.NewEncoder(w).Encode(todo) | |
} | |
func main() { | |
r := chi.NewRouter() | |
r.Use(withUser) | |
// http://localhost:8081/todos -> Forbidden | |
// http://localhost:8081/todos?user=1 -> OK | |
r.With(withTodos, canViewTodos).Get("/todos", index) | |
// http://localhost:8081/todos/1?user=1 -> OK | |
// http://localhost:8081/todos/2?user=2 -> OK | |
// http://localhost:8081/todos/1?user=2 -> Forbidden | |
// http://localhost:8081/todos/2?user=1 -> Forbidden | |
// http://localhost:8081/todos/3?user=1 -> Not found | |
r.With(withTodo, canUpdateTodo).Get("/todos/{todoID}", update) | |
http.ListenAndServe(":8081", r) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment