package server import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "net/http" "os" "time" "git.t-juice.club/microfilm/auth" "git.t-juice.club/microfilm/auth/authmw" "git.t-juice.club/microfilm/users" "git.t-juice.club/microfilm/users/store" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/nats-io/nats.go" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" ) type UserServer struct { http.Server store store.UserStore config *Config nats *nats.Conn Logger *slog.Logger } func NewServer(config *Config) (*UserServer, error) { r := chi.NewRouter() srv := &UserServer{} tp, err := tracerProvider("jaeger:4318") if err != nil { return nil, err } otel.SetTracerProvider(tp) srv.config = config srv.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, })) r.Use(srv.MiddlewareLogging) r.Use(srv.MiddlewareTracing) verifyAdmin := authmw.VerifyToken("http://mf-auth:8082", []string{auth.RoleAdmin}) r.Get("/info", InfoHandler) r.With(verifyAdmin).Post("/", srv.CreateUserHandler) r.Get("/{identifier}", srv.GetUserHandler) r.Post("/{identifier}/password", srv.SetPasswordHandler) r.Post("/{identifier}/verify", srv.VerifyHandler) srv.Addr = config.ListenAddr srv.Handler = r srv.store = store.NewMemoryStore() // Add initial admin-user u := users.User{ ID: uuid.Must(uuid.NewRandom()).String(), Username: "admin", Role: "admin", } password := uuid.Must(uuid.NewRandom()).String() _ = u.SetPassword(password) _ = srv.store.AddUser(u) srv.Logger.Warn("Initial admin-user created.", "username", u.Username, "password", password) conn, err := nats.Connect(config.NATSAddr) if err != nil { return nil, err } srv.nats = conn return srv, nil } func tracerProvider(url string) (*sdktrace.TracerProvider, error) { exp, err := otlptracehttp.New(context.Background(), otlptracehttp.WithEndpoint(url), otlptracehttp.WithInsecure()) if err != nil { return nil, err } res := resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName("mf-users"), semconv.ServiceVersion(users.Version), ) tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(time.Second)), sdktrace.WithResource(res), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) return tp, nil } func InfoHandler(w http.ResponseWriter, r *http.Request) { enc := json.NewEncoder(w) data := &users.InfoResponse{ Version: users.Version, } _ = enc.Encode(data) } func WriteError(w http.ResponseWriter, response users.ErrorResponse) { w.WriteHeader(response.Status) encoder := json.NewEncoder(w) _ = encoder.Encode(&response) } func (s *UserServer) CreateUserHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) defer r.Body.Close() var request users.CreateUserRequest if err := decoder.Decode(&request); err != nil { WriteError(w, users.ErrorResponse{ Status: http.StatusBadRequest, Message: fmt.Sprintf("Error parsing request: %s", err), }) return } id := uuid.Must(uuid.NewRandom()) u := users.User{ ID: id.String(), Username: request.Username, } if err := u.SetPassword(request.Password); err != nil { WriteError(w, users.ErrorResponse{ Status: http.StatusInternalServerError, Message: fmt.Sprintf("Error setting user password: %s", err), }) return } if err := s.store.AddUser(u); err != nil { s.Logger.Warn("Error storing user", "error", err) WriteError(w, users.ErrorResponse{ Status: http.StatusInternalServerError, Message: fmt.Sprintf("Error storing user: %s", err), }) return } // Message sub := fmt.Sprintf("%s.%s", s.config.NATSSubject, "create") var buf bytes.Buffer msg := &users.MsgUserCreate{ Message: "User created.", User: u, } encoder := json.NewEncoder(&buf) _ = encoder.Encode(&msg) if err := s.nats.Publish(sub, buf.Bytes()); err != nil { s.Logger.Warn("Error publishing message", "error", err) } s.Logger.Info("User created.", "username", u.Username, "id", u.ID) response := &users.CreateUserResponse{ Message: "User created.", User: u, } encoder = json.NewEncoder(w) _ = encoder.Encode(&response) } func (s *UserServer) GetUserHandler(w http.ResponseWriter, r *http.Request) { identifier := chi.URLParam(r, "identifier") u, err := s.store.GetUser(identifier) if err != nil { switch err { case store.ErrNoSuchUser: WriteError(w, users.ErrorResponse{ Message: fmt.Sprintf("No such user: %s", identifier), Status: http.StatusNotFound, }) return } WriteError(w, users.ErrorResponse{ Message: fmt.Sprintf("Unable to get user: %s", err), Status: http.StatusInternalServerError, }) return } encoder := json.NewEncoder(w) _ = encoder.Encode(&u) } func (s *UserServer) SetPasswordHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) defer r.Body.Close() var request users.SetPasswordRequest if err := decoder.Decode(&request); err != nil { WriteError(w, users.ErrorResponse{ Status: http.StatusBadRequest, Message: fmt.Sprintf("Error parsing request: %s", err), }) return } id := chi.URLParam(r, "identifier") if id == "" { WriteError(w, users.ErrorResponse{ Status: http.StatusBadRequest, Message: fmt.Sprintf("Invalid user ID: %s", id), }) return } u, err := s.store.GetUser(id) if err != nil { msg := fmt.Sprintf("Server error: %s", err) status := http.StatusInternalServerError switch err { case store.ErrNoSuchUser: msg = "No user with that ID" status = http.StatusNotFound } WriteError(w, users.ErrorResponse{ Status: status, Message: msg, }) return } if err := u.SetPassword(request.NewPassword); err != nil { WriteError(w, users.ErrorResponse{ Status: http.StatusBadRequest, Message: fmt.Sprintf("Unable to set password: %s", id), }) return } if err := s.store.UpdateUser(u); err != nil { s.Logger.Warn("Unable to update user.", "id", u.ID, "error", err) WriteError(w, users.ErrorResponse{ Status: http.StatusInternalServerError, Message: fmt.Sprintf("Unable to set password: %s", id), }) return } sub := fmt.Sprintf("%s.%s", s.config.NATSSubject, "update") var buf bytes.Buffer encoder := json.NewEncoder(&buf) _ = encoder.Encode(&users.MsgUserUpdate{Message: "Password updated", ID: u.ID}) if err := s.nats.Publish(sub, buf.Bytes()); err != nil { s.Logger.Warn("Error publishing message", "error", err) } s.Logger.Info("User password updated.", "id", u.ID) } func (s *UserServer) VerifyHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) defer r.Body.Close() var request users.VerifyRequest if err := decoder.Decode(&request); err != nil { WriteError(w, users.ErrorResponse{ Status: http.StatusBadRequest, Message: fmt.Sprintf("Error parsing request: %s", err), }) return } id := chi.URLParam(r, "identifier") if id == "" { WriteError(w, users.ErrorResponse{ Status: http.StatusBadRequest, Message: fmt.Sprintf("Invalid user ID: %s", id), }) return } u, err := s.store.GetUser(id) if err != nil { msg := fmt.Sprintf("Server error: %s", err) status := http.StatusInternalServerError switch err { case store.ErrNoSuchUser: msg = "No user with that ID" status = http.StatusNotFound } WriteError(w, users.ErrorResponse{ Status: status, Message: msg, }) return } err = u.ComparePassword(request.Password) if err != nil { WriteError(w, users.ErrorResponse{ Status: http.StatusUnauthorized, Message: "Password verification failed.", }) return } w.WriteHeader(http.StatusOK) }