package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
)
type User struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Status string `json:"status"`
Department string `json:"department,omitempty"`
CreatedAt time.Time `json:"created_at"`
LastLogin *time.Time `json:"last_login,omitempty"`
}
type Role struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
type Group struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
type Member struct {
UserID string `json:"user_id"`
}
type CreateUserRequest struct {
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
}
type ListResponse[T any] struct {
Data []T `json:"data"`
NextCursor string `json:"next_cursor"`
}
type SingleResponse[T any] struct {
Data T `json:"data"`
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
type GroupAssignment struct {
GroupID string `json:"group_id"`
}
type RoleAssignment struct {
RoleID string `json:"role_id"`
}
type store struct {
mu sync.RWMutex
users []User
roles []Role
groups []Group
roleMembers map[string][]Member
groupMembers map[string][]Member
roleGroups map[string][]GroupAssignment
roleRoles map[string][]RoleAssignment
nextUserSeq int
}
func newStore() *store {
now := time.Now()
return &store{
users: []User{
{ID: "u1", Email: "alice@appville.com", FirstName: "Alice", LastName: "Smith", Username: "alice", Status: "active", CreatedAt: now},
{ID: "u2", Email: "bob@appville.com", FirstName: "Bob", LastName: "Jones", Username: "bob", Status: "active", CreatedAt: now},
{ID: "u3", Email: "carol@appville.com", FirstName: "Carol", LastName: "White", Username: "carol", Status: "active", CreatedAt: now},
{ID: "u4", Email: "dave@appville.com", FirstName: "Dave", LastName: "Brown", Username: "dave", Status: "active", CreatedAt: now},
{ID: "u5", Email: "eve@appville.com", FirstName: "Eve", LastName: "Davis", Username: "eve", Status: "active", CreatedAt: now},
{ID: "u6", Email: "frank@appville.com", FirstName: "Frank", LastName: "Miller", Username: "frank", Status: "active", CreatedAt: now},
},
roles: []Role{
{ID: "r1", Name: "Single Ride", Description: "One-time journey on any standard route"},
{ID: "r2", Name: "Standard Day", Description: "Unlimited standard travel for the day"},
{ID: "r3", Name: "Dining Access", Description: "Access to the dining car service"},
{ID: "r4", Name: "First Class", Description: "Premium seating with complimentary dining"},
{ID: "r5", Name: "Express", Description: "Access to express routes with priority boarding"},
},
groups: []Group{
{ID: "g1", Name: "Regional Pass", Description: "Monthly pass for standard daily travel"},
{ID: "g2", Name: "Express Pass", Description: "Monthly pass for express routes"},
{ID: "g3", Name: "All-Access Pass", Description: "Premium monthly pass with first-class travel and dining"},
},
roleMembers: map[string][]Member{
"r1": {{UserID: "u6"}},
"r2": {{UserID: "u4"}},
"r3": {{UserID: "u2"}, {UserID: "u3"}, {UserID: "u4"}},
"r4": {{UserID: "u5"}},
},
groupMembers: map[string][]Member{
"g1": {{UserID: "u2"}},
"g2": {{UserID: "u3"}},
"g3": {{UserID: "u1"}},
},
roleGroups: map[string][]GroupAssignment{
"r2": {{GroupID: "g1"}},
"r4": {{GroupID: "g3"}},
"r5": {{GroupID: "g2"}},
},
roleRoles: map[string][]RoleAssignment{
"r3": {{RoleID: "r4"}},
},
nextUserSeq: 7,
}
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
log.Printf("ERROR writing JSON response: %v", err)
}
}
func logRequest(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL)
next(w, r)
}
}
func main() {
port := flag.Int("port", 8765, "port to listen on")
flag.Parse()
s := newStore()
mux := http.NewServeMux()
mux.HandleFunc("/oauth/token", logRequest(func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, TokenResponse{
AccessToken: "mock-server-token",
ExpiresIn: 3600,
TokenType: "Bearer",
})
}))
mux.HandleFunc("/api/users/me", logRequest(func(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
writeJSON(w, http.StatusOK, SingleResponse[User]{Data: s.users[0]})
}))
mux.HandleFunc("/api/users", logRequest(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
s.mu.Lock()
id := fmt.Sprintf("u%d", s.nextUserSeq)
s.nextUserSeq++
u := User{
ID: id,
Email: req.Email,
FirstName: req.FirstName,
LastName: req.LastName,
Username: req.Username,
Status: "active",
CreatedAt: time.Now(),
}
s.users = append(s.users, u)
s.mu.Unlock()
log.Printf(" -> created user %s (%s)", id, req.Email)
writeJSON(w, http.StatusCreated, SingleResponse[User]{Data: u})
case http.MethodGet:
s.mu.RLock()
defer s.mu.RUnlock()
const pageSize = 2
cursor := r.URL.Query().Get("cursor")
start := 0
if cursor != "" {
for i, u := range s.users {
if u.ID == cursor {
start = i
break
}
}
}
end := start + pageSize
if end > len(s.users) {
end = len(s.users)
}
next := ""
if end < len(s.users) {
next = s.users[end].ID
}
writeJSON(w, http.StatusOK, ListResponse[User]{
Data: s.users[start:end],
NextCursor: next,
})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}))
mux.HandleFunc("/api/users/", logRequest(func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/users/")
if id == "" || id == "me" {
return
}
s.mu.Lock()
defer s.mu.Unlock()
idx := -1
for i, u := range s.users {
if u.ID == id {
idx = i
break
}
}
if idx == -1 {
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodGet:
writeJSON(w, http.StatusOK, SingleResponse[User]{Data: s.users[idx]})
case http.MethodPatch:
var attrs map[string]string
if err := json.NewDecoder(r.Body).Decode(&attrs); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if v, ok := attrs["status"]; ok {
s.users[idx].Status = v
}
if v, ok := attrs["first_name"]; ok {
s.users[idx].FirstName = v
}
if v, ok := attrs["last_name"]; ok {
s.users[idx].LastName = v
}
if v, ok := attrs["email"]; ok {
s.users[idx].Email = v
}
if v, ok := attrs["department"]; ok {
s.users[idx].Department = v
}
log.Printf(" -> updated user %s", id)
writeJSON(w, http.StatusOK, SingleResponse[User]{Data: s.users[idx]})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}))
mux.HandleFunc("/api/roles", logRequest(func(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
writeJSON(w, http.StatusOK, ListResponse[Role]{Data: s.roles})
}))
mux.HandleFunc("/api/roles/", logRequest(func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/roles/"), "/")
roleID := parts[0]
if len(parts) >= 2 && parts[1] == "members" {
if len(parts) == 3 {
userID := parts[2]
s.mu.Lock()
defer s.mu.Unlock()
switch r.Method {
case http.MethodPut:
members := s.roleMembers[roleID]
for _, m := range members {
if m.UserID == userID {
w.WriteHeader(http.StatusNoContent)
return
}
}
s.roleMembers[roleID] = append(s.roleMembers[roleID], Member{UserID: userID})
log.Printf(" -> added %s to role %s", userID, roleID)
w.WriteHeader(http.StatusNoContent)
case http.MethodDelete:
members := s.roleMembers[roleID]
for i, m := range members {
if m.UserID == userID {
s.roleMembers[roleID] = append(members[:i], members[i+1:]...)
break
}
}
log.Printf(" -> removed %s from role %s", userID, roleID)
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
return
}
s.mu.RLock()
defer s.mu.RUnlock()
writeJSON(w, http.StatusOK, ListResponse[Member]{Data: s.roleMembers[roleID]})
return
}
if len(parts) == 2 && parts[1] == "groups" {
s.mu.RLock()
defer s.mu.RUnlock()
writeJSON(w, http.StatusOK, ListResponse[GroupAssignment]{Data: s.roleGroups[roleID]})
return
}
if len(parts) == 2 && parts[1] == "roles" {
s.mu.RLock()
defer s.mu.RUnlock()
writeJSON(w, http.StatusOK, ListResponse[RoleAssignment]{Data: s.roleRoles[roleID]})
return
}
if len(parts) == 1 {
s.mu.RLock()
defer s.mu.RUnlock()
for _, rl := range s.roles {
if rl.ID == roleID {
writeJSON(w, http.StatusOK, SingleResponse[Role]{Data: rl})
return
}
}
}
http.NotFound(w, r)
}))
mux.HandleFunc("/api/groups", logRequest(func(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
writeJSON(w, http.StatusOK, ListResponse[Group]{Data: s.groups})
}))
mux.HandleFunc("/api/groups/", logRequest(func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/groups/"), "/")
groupID := parts[0]
if len(parts) >= 2 && parts[1] == "members" {
if len(parts) == 3 {
userID := parts[2]
s.mu.Lock()
defer s.mu.Unlock()
switch r.Method {
case http.MethodPut:
members := s.groupMembers[groupID]
for _, m := range members {
if m.UserID == userID {
w.WriteHeader(http.StatusNoContent)
return
}
}
s.groupMembers[groupID] = append(s.groupMembers[groupID], Member{UserID: userID})
log.Printf(" -> added %s to group %s", userID, groupID)
w.WriteHeader(http.StatusNoContent)
case http.MethodDelete:
members := s.groupMembers[groupID]
for i, m := range members {
if m.UserID == userID {
s.groupMembers[groupID] = append(members[:i], members[i+1:]...)
break
}
}
log.Printf(" -> removed %s from group %s", userID, groupID)
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
return
}
s.mu.RLock()
defer s.mu.RUnlock()
writeJSON(w, http.StatusOK, ListResponse[Member]{Data: s.groupMembers[groupID]})
return
}
if len(parts) == 1 {
s.mu.RLock()
defer s.mu.RUnlock()
for _, g := range s.groups {
if g.ID == groupID {
writeJSON(w, http.StatusOK, SingleResponse[Group]{Data: g})
return
}
}
}
http.NotFound(w, r)
}))
addr := fmt.Sprintf(":%d", *port)
log.Printf("Mock API server starting on http://localhost%s", addr)
log.Printf("OAuth token endpoint: POST /oauth/token")
log.Printf("Users: GET /api/users, POST /api/users, GET/PATCH /api/users/{id}")
log.Printf("Roles: GET /api/roles, GET /api/roles/{id}, GET /api/roles/{id}/members")
log.Printf(" PUT /api/roles/{id}/members/{uid}, DELETE /api/roles/{id}/members/{uid}")
log.Printf(" GET /api/roles/{id}/groups, GET /api/roles/{id}/roles")
log.Printf("Groups: GET /api/groups, GET /api/groups/{id}, GET /api/groups/{id}/members")
log.Printf(" PUT /api/groups/{id}/members/{uid}, DELETE /api/groups/{id}/members/{uid}")
log.Fatal(http.ListenAndServe(addr, mux))
}