|
1 | 1 | (ns parts.server
|
| 2 | + "Primary namespace for the Parts server setup, handling route definitions, |
| 3 | + middleware configuration, and server lifecycle management. This namespace |
| 4 | + organizes the application's API routes with appropriate content types |
| 5 | + and authentication controls." |
2 | 6 | (:require
|
3 | 7 | [clojure.core.async :as async]
|
4 | 8 | [com.brunobonacci.mulog :as mulog]
|
|
7 | 11 | [parts.api.auth :as api.auth]
|
8 | 12 | [parts.api.systems :as api.systems]
|
9 | 13 | [parts.auth :as auth]
|
10 |
| - [parts.config :as config] |
11 | 14 | [parts.db :as db]
|
12 | 15 | [parts.handlers.pages :as pages]
|
13 | 16 | [parts.handlers.waitlist :as waitlist]
|
14 | 17 | [parts.middleware :as middleware]
|
15 |
| - [reitit.coercion.spec] |
| 18 | + [reitit.coercion.spec :as rcs] |
16 | 19 | [reitit.ring :as ring]
|
17 |
| - [reitit.swagger :as swagger] |
18 |
| - [reitit.swagger-ui :as swagger-ui] |
19 |
| - [ring.middleware.json :refer [wrap-json-body wrap-json-response]] |
| 20 | + [reitit.ring.coercion :as rrc] |
| 21 | + [reitit.ring.middleware.muuntaja :as muuntaja-middleware] |
| 22 | + [muuntaja.core :as muuntaja] |
20 | 23 | [ring.middleware.params :refer [wrap-params]])
|
21 | 24 | (:gen-class))
|
22 | 25 |
|
| 26 | +;; ===== Content Type Configuration ===== |
| 27 | + |
| 28 | +(def transit-format |
| 29 | + "A configured Muuntaja instance that restricts API format negotiation to |
| 30 | + Transit only (application/transit+json). This instance transforms Clojure |
| 31 | + data structures to/from Transit format when used with appropriate middleware. |
| 32 | + The default format is explicitly set to Transit, so requests without |
| 33 | + an Accept header will receive Transit responses." |
| 34 | + (-> muuntaja/default-options |
| 35 | + (update :formats select-keys ["application/transit+json"]) |
| 36 | + (assoc :default-format "application/transit+json") |
| 37 | + muuntaja/create)) |
| 38 | + |
| 39 | +;; ===== Middleware Configuration ===== |
| 40 | + |
| 41 | +(def base-middleware |
| 42 | + "Common middleware applied to all routes for essential functionality." |
| 43 | + [wrap-params |
| 44 | + middleware/exception |
| 45 | + middleware/logging]) |
| 46 | + |
| 47 | +(def html-middleware |
| 48 | + "Middleware specific to HTML routes that ensures responses are properly |
| 49 | + formatted as text/html." |
| 50 | + {:data {:middleware (into base-middleware |
| 51 | + [middleware/wrap-html-response])}}) |
| 52 | + |
| 53 | +(def api-middleware |
| 54 | + "Middleware for API endpoints that need Transit formatting. |
| 55 | + Includes JWT authentication setup but doesn't force authentication |
| 56 | + (individual routes can require auth with the with-auth helper)." |
| 57 | + {:data {:middleware (into base-middleware |
| 58 | + [muuntaja-middleware/format-middleware |
| 59 | + rrc/coerce-exceptions-middleware |
| 60 | + rrc/coerce-request-middleware |
| 61 | + rrc/coerce-response-middleware |
| 62 | + middleware/wrap-jwt-authentication]) |
| 63 | + :muuntaja transit-format |
| 64 | + :coercion rcs/coercion}}) |
| 65 | + |
| 66 | +(defn with-auth |
| 67 | + "Helper function to require authentication for a route." |
| 68 | + [route-data] |
| 69 | + (update route-data :middleware (fnil conj []) middleware/jwt-auth)) |
| 70 | + |
| 71 | +;; ===== Route Definitions ===== |
| 72 | + |
23 | 73 | (def html-routes
|
| 74 | + "Public routes that return HTML content. These routes: |
| 75 | + - Don't require authentication |
| 76 | + - Return text/html content type |
| 77 | + - Use the html-middleware stack" |
24 | 78 | [["/" {:get {:handler #(pages/home-page %)}}]
|
25 | 79 | ["/system" {:get {:handler #(pages/system-graph %)}}]
|
26 | 80 | ["/up" {:get {:handler (fn [_] {:status 200 :body "OK"})}}]
|
27 | 81 | ["/waitlist-signup" {:post {:handler #(waitlist/signup %)}}]])
|
28 | 82 |
|
29 | 83 | (def api-routes
|
30 |
| - [["/swagger.json" |
31 |
| - {:get {:no-doc true |
32 |
| - :swagger {:info {:title "Parts API" |
33 |
| - :description "API for Parts"}} |
34 |
| - :handler (swagger/create-swagger-handler)}}] |
35 |
| - ["/api" |
| 84 | + "API routes that return Transit data. Contains both public and authenticated routes. |
| 85 | + All routes: |
| 86 | + - Return application/transit+json content type |
| 87 | + - Use the api-middleware stack |
| 88 | +
|
| 89 | + Authenticated routes use the with-auth helper to require authentication." |
| 90 | + [["/api" |
36 | 91 | ["/ping"
|
37 |
| - {:get {:swagger {:tags ["Utility"]} |
38 |
| - :handler (fn [_] {:status 200 :body {:message "Pong!"}})}}] |
| 92 | + {:get {:handler (fn [_] {:status 200 :body {:message "Pong!"}})}}] |
39 | 93 |
|
40 | 94 | ;; Auth routes
|
41 |
| - ["/auth" {:swagger {:tags ["Authentication"]}} |
| 95 | + ["/auth" |
42 | 96 | ["/login"
|
43 | 97 | {:post {:handler api.auth/login}}]
|
44 | 98 | ["/refresh"
|
45 | 99 | {:post {:handler api.auth/refresh}}]
|
46 | 100 | ["/logout"
|
47 |
| - {:post {:handler api.auth/logout |
48 |
| - :middleware [middleware/jwt-auth]}}]] |
| 101 | + {:post (with-auth {:handler api.auth/logout})}]] |
49 | 102 |
|
50 | 103 | ;; Account routes
|
51 |
| - ["/account" {:swagger {:tags ["Account"]}} |
52 |
| - ["" |
53 |
| - {:get {:handler api.account/get-account} |
54 |
| - :patch {:handler api.account/update-account} |
55 |
| - :delete {:handler api.account/delete-account} |
56 |
| - :middleware [middleware/jwt-auth]}] |
| 104 | + ["/account" |
57 | 105 | ["/register"
|
58 |
| - {:post {:handler api.account/register-account}}]] |
| 106 | + {:post {:handler api.account/register-account}}] |
| 107 | + ["" |
| 108 | + (with-auth {:get {:handler api.account/get-account} |
| 109 | + :patch {:handler api.account/update-account} |
| 110 | + :delete {:handler api.account/delete-account}})]] |
59 | 111 |
|
60 | 112 | ;; Systems routes
|
61 |
| - ;; TODO: Put these behind jwt-auth middleware once the UI is ready |
62 |
| - ["/systems" {:swagger {:tags ["Systems"]} |
63 |
| - :middleware [middleware/jwt-auth]} |
64 |
| - ["" {:get {:summary "List all systems for current user" |
65 |
| - :handler api.systems/list-systems} |
66 |
| - :post {:summary "Create new system" |
67 |
| - :handler api.systems/create-system}}] |
| 113 | + ["/systems" |
| 114 | + ["" {:get {:handler api.systems/list-systems} |
| 115 | + :post {:handler api.systems/create-system}}] |
68 | 116 | ["/:id" {:parameters {:path {:id string?}}}
|
69 |
| - ["" {:get {:summary "Get system by ID" |
70 |
| - :handler api.systems/get-system} |
71 |
| - :put {:summary "Update entire system" |
72 |
| - :handler api.systems/update-system} |
73 |
| - :delete {:summary "Delete system" |
74 |
| - :handler api.systems/delete-system}}] |
75 |
| - ["/pdf" {:get {:summary "Generate PDF export of system" |
76 |
| - :handler api.systems/export-pdf}}]]]]]) |
77 |
| - |
78 |
| -(defn create-handler [routes middleware-config swagger?] |
79 |
| - (ring/ring-handler |
80 |
| - (ring/router routes middleware-config) |
81 |
| - (if swagger? |
82 |
| - (ring/routes |
83 |
| - (swagger-ui/create-swagger-ui-handler |
84 |
| - {:path "/swagger-ui" |
85 |
| - :url "/swagger.json" |
86 |
| - :config {:validatorUrl nil |
87 |
| - :operationsSorter "alpha"}}) |
88 |
| - (ring/create-default-handler)) |
89 |
| - (ring/create-default-handler)))) |
| 117 | + ["" {:get {:handler api.systems/get-system} |
| 118 | + :put {:handler api.systems/update-system} |
| 119 | + :delete {:handler api.systems/delete-system}}] |
| 120 | + ["/pdf" {:get {:handler api.systems/export-pdf}}]]]]]) |
90 | 121 |
|
91 |
| -(def html-middleware |
92 |
| - {:data {:middleware [wrap-params |
93 |
| - middleware/exception |
94 |
| - middleware/logging |
95 |
| - middleware/wrap-html-response]}}) |
| 122 | +;; ===== Router Configuration ===== |
96 | 123 |
|
97 |
| -(def api-middleware |
98 |
| - {:data {:middleware [wrap-params |
99 |
| - middleware/exception |
100 |
| - middleware/logging |
101 |
| - [wrap-json-body {:keywords? true}] |
102 |
| - wrap-json-response |
103 |
| - middleware/wrap-jwt-authentication] |
104 |
| - :coercion reitit.coercion.spec/coercion}}) |
105 |
| - |
106 |
| -(defn app [] |
107 |
| - (let [routes (if (config/dev?) |
108 |
| - (concat html-routes api-routes) |
109 |
| - html-routes) |
110 |
| - middleware (if (config/dev?) |
111 |
| - (-> html-middleware |
112 |
| - (update-in [:data :middleware] concat |
113 |
| - (get-in api-middleware [:data :middleware])) |
114 |
| - (assoc-in [:data :coercion] |
115 |
| - (get-in api-middleware [:data :coercion]))) |
116 |
| - html-middleware)] |
117 |
| - (middleware/wrap-default-middlewares |
118 |
| - (create-handler routes middleware (config/dev?))))) |
| 124 | +(defn app |
| 125 | + "Constructs the application's handler function by combining HTML and API routes |
| 126 | + with their appropriate middleware configurations. The function: |
| 127 | +
|
| 128 | + 1. Creates a single router with all routes, but preserves the middleware |
| 129 | + configurations for each route type using Reitit's route data mechanism |
| 130 | +
|
| 131 | + 2. Wraps the router with the default application middlewares |
| 132 | +
|
| 133 | + This approach ensures proper routing while maintaining the distinct middleware |
| 134 | + needs of HTML and API routes." |
| 135 | + [] |
| 136 | + (-> |
| 137 | + ;; Create a single router with both HTML and API routes |
| 138 | + ;; Each route maintains its specific middleware config through route data |
| 139 | + (ring/router |
| 140 | + (concat |
| 141 | + ;; Apply html-middleware to all HTML routes |
| 142 | + (for [route html-routes] |
| 143 | + (let [[path data] route] |
| 144 | + [path (merge-with merge data (:data html-middleware))])) |
| 145 | + |
| 146 | + ;; Apply api-middleware to all API routes |
| 147 | + api-routes) |
| 148 | + ;; Include the api-middleware configuration for API routes |
| 149 | + api-middleware) |
| 150 | + |
| 151 | + ;; Create handler with default not-found |
| 152 | + (ring/ring-handler (ring/create-default-handler)) |
| 153 | + |
| 154 | + ;; Apply application-wide middleware |
| 155 | + middleware/wrap-default-middlewares)) |
119 | 156 |
|
120 | 157 | (defn schedule-token-cleanup
|
121 | 158 | "Schedule periodic cleanup of expired refresh tokens using core.async"
|
|
140 | 177 | stop-ch))
|
141 | 178 |
|
142 | 179 | (defn start-server
|
143 |
| - "Starts the web server" |
| 180 | + "Starts the web server with the configured application handler. |
| 181 | + Returns a function that can be called to stop the server." |
144 | 182 | [port]
|
145 | 183 | (mulog/log ::starting-server :port port)
|
146 | 184 | (server/run-server (app) {:port port}))
|
147 | 185 |
|
148 | 186 | (defn -main
|
149 |
| - "Entry point into the application via clojure.main -M" |
| 187 | + "Entry point into the application via clojure.main -M. |
| 188 | + Initializes the application, starts the server, and returns a shutdown function. |
| 189 | +
|
| 190 | + Arguments: |
| 191 | + - Optional first argument: port number (defaults to 3000)" |
150 | 192 | [& args]
|
151 | 193 | (let [port (or (some-> (first args) Integer/parseInt) 3000)]
|
| 194 | + ;; Set up global logging context |
152 | 195 | (mulog/set-global-context!
|
153 | 196 | {:app-name "Parts" :version "0.1.0-SNAPSHOT"})
|
154 | 197 | (mulog/log ::application-startup :arguments args :port port)
|
| 198 | + |
| 199 | + ;; Initialize database |
155 | 200 | (db/init-db)
|
| 201 | + |
| 202 | + ;; Start server and background processes |
156 | 203 | (let [stop-fn (start-server port)
|
157 | 204 | cleanup-stop-ch (schedule-token-cleanup)]
|
158 | 205 | (println "Parts: Server started on port" port)
|
| 206 | + |
| 207 | + ;; Return shutdown function |
159 | 208 | (fn []
|
160 | 209 | (stop-fn)
|
161 | 210 | (async/close! cleanup-stop-ch) ; Signal the cleanup process to stop
|
|
0 commit comments