mgmt_api.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. package http
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "net"
  10. "net/http"
  11. "os"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/google/uuid"
  16. "github.com/mk6i/retro-aim-server/config"
  17. "github.com/mk6i/retro-aim-server/state"
  18. "github.com/mk6i/retro-aim-server/wire"
  19. )
  20. func StartManagementAPI(
  21. bld config.Build,
  22. cfg config.Config,
  23. userManager UserManager,
  24. sessionRetriever SessionRetriever,
  25. chatRoomRetriever ChatRoomRetriever,
  26. chatRoomCreator ChatRoomCreator,
  27. chatSessionRetriever ChatSessionRetriever,
  28. directoryManager DirectoryManager,
  29. messageRelayer MessageRelayer,
  30. bartRetriever BARTRetriever,
  31. feedbagRetriever FeedBagRetriever,
  32. accountRetriever AccountRetriever,
  33. profileRetriever ProfileRetriever,
  34. logger *slog.Logger,
  35. ) {
  36. mux := http.NewServeMux()
  37. // Handlers for '/user' route
  38. mux.HandleFunc("DELETE /user", func(w http.ResponseWriter, r *http.Request) {
  39. deleteUserHandler(w, r, userManager, logger)
  40. })
  41. mux.HandleFunc("GET /user", func(w http.ResponseWriter, r *http.Request) {
  42. getUserHandler(w, userManager, logger)
  43. })
  44. mux.HandleFunc("POST /user", func(w http.ResponseWriter, r *http.Request) {
  45. postUserHandler(w, r, userManager, uuid.New, logger)
  46. })
  47. // Handlers for '/user/password' route
  48. mux.HandleFunc("PUT /user/password", func(w http.ResponseWriter, r *http.Request) {
  49. putUserPasswordHandler(w, r, userManager, logger)
  50. })
  51. // Handlers for '/user/login' route
  52. mux.HandleFunc("GET /user/login", func(w http.ResponseWriter, r *http.Request) {
  53. getUserLoginHandler(w, r, userManager, logger)
  54. })
  55. // Handlers for '/user/{screenname}/account' route
  56. mux.HandleFunc("GET /user/{screenname}/account", func(w http.ResponseWriter, r *http.Request) {
  57. getUserAccountHandler(w, r, userManager, accountRetriever, profileRetriever, logger)
  58. })
  59. // Handlers for '/user/{screenname}/icon' route
  60. mux.HandleFunc("GET /user/{screenname}/icon", func(w http.ResponseWriter, r *http.Request) {
  61. getUserBuddyIconHandler(w, r, userManager, feedbagRetriever, bartRetriever, logger)
  62. })
  63. // Handlers for '/session' route
  64. mux.HandleFunc("GET /session", func(w http.ResponseWriter, r *http.Request) {
  65. getSessionHandler(w, r, sessionRetriever, time.Since)
  66. })
  67. // Handlers for '/session/{screenname}' route
  68. mux.HandleFunc("GET /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  69. getSessionHandler(w, r, sessionRetriever, time.Since)
  70. })
  71. // Handlers for '/chat/room/public' route
  72. mux.HandleFunc("GET /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  73. getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  74. })
  75. mux.HandleFunc("POST /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  76. postPublicChatHandler(w, r, chatRoomCreator, logger)
  77. })
  78. // Handlers for '/chat/room/private' route
  79. mux.HandleFunc("GET /chat/room/private", func(w http.ResponseWriter, r *http.Request) {
  80. getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  81. })
  82. // Handlers for '/instant-message' route
  83. mux.HandleFunc("POST /instant-message", func(w http.ResponseWriter, r *http.Request) {
  84. postInstantMessageHandler(w, r, messageRelayer, logger)
  85. })
  86. // Handlers for '/version' route
  87. mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) {
  88. getVersionHandler(w, bld)
  89. })
  90. // Handlers for '/directory/category' route
  91. mux.HandleFunc("GET /directory/category", func(w http.ResponseWriter, r *http.Request) {
  92. getDirectoryCategoryHandler(w, directoryManager, logger)
  93. })
  94. mux.HandleFunc("POST /directory/category", func(w http.ResponseWriter, r *http.Request) {
  95. postDirectoryCategoryHandler(w, r, directoryManager, logger)
  96. })
  97. // Handlers for '/directory/category/{id}' route
  98. mux.HandleFunc("DELETE /directory/category/{id}", func(w http.ResponseWriter, r *http.Request) {
  99. deleteDirectoryCategoryHandler(w, r, directoryManager, logger)
  100. })
  101. // Handlers for '/directory/category/{id}/keyword' route
  102. mux.HandleFunc("GET /directory/category/{id}/keyword", func(w http.ResponseWriter, r *http.Request) {
  103. getDirectoryCategoryKeywordHandler(w, r, directoryManager, logger)
  104. })
  105. // Handlers for '/directory/keyword' route
  106. mux.HandleFunc("POST /directory/keyword", func(w http.ResponseWriter, r *http.Request) {
  107. postDirectoryKeywordHandler(w, r, directoryManager, logger)
  108. })
  109. // Handlers for '/directory/keyword/{id}' route
  110. mux.HandleFunc("DELETE /directory/keyword/{id}", func(w http.ResponseWriter, r *http.Request) {
  111. deleteDirectoryKeywordHandler(w, r, directoryManager, logger)
  112. })
  113. addr := net.JoinHostPort(cfg.ApiHost, cfg.ApiPort)
  114. logger.Info("starting management API server", "addr", addr)
  115. if err := http.ListenAndServe(addr, mux); err != nil {
  116. logger.Error("unable to bind management API address address", "err", err.Error())
  117. os.Exit(1)
  118. }
  119. }
  120. // deleteUserHandler handles the DELETE /user endpoint.
  121. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  122. user, err := userFromBody(r)
  123. if err != nil {
  124. http.Error(w, err.Error(), http.StatusBadRequest)
  125. return
  126. }
  127. err = manager.DeleteUser(state.NewIdentScreenName(user.ScreenName))
  128. switch {
  129. case errors.Is(err, state.ErrNoUser):
  130. http.Error(w, "user does not exist", http.StatusNotFound)
  131. return
  132. case err != nil:
  133. logger.Error("error deleting user DELETE /user", "err", err.Error())
  134. http.Error(w, "internal server error", http.StatusInternalServerError)
  135. return
  136. }
  137. w.WriteHeader(http.StatusNoContent)
  138. _, _ = fmt.Fprintln(w, "User account successfully deleted.")
  139. }
  140. // putUserPasswordHandler handles the PUT /user/password endpoint.
  141. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  142. input, err := userFromBody(r)
  143. if err != nil {
  144. http.Error(w, err.Error(), http.StatusBadRequest)
  145. return
  146. }
  147. sn := state.NewIdentScreenName(input.ScreenName)
  148. if err := userManager.SetUserPassword(sn, input.Password); err != nil {
  149. switch {
  150. case errors.Is(err, state.ErrNoUser):
  151. http.Error(w, "user does not exist", http.StatusNotFound)
  152. return
  153. case errors.Is(err, state.ErrPasswordInvalid):
  154. http.Error(w, err.Error(), http.StatusBadRequest)
  155. return
  156. default:
  157. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  158. http.Error(w, "internal server error", http.StatusInternalServerError)
  159. return
  160. }
  161. }
  162. w.WriteHeader(http.StatusNoContent)
  163. _, _ = fmt.Fprintln(w, "Password successfully reset.")
  164. }
  165. // getSessionHandler handles GET /session
  166. func getSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever, funcTimeSince func(t time.Time) time.Duration) {
  167. w.Header().Set("Content-Type", "application/json")
  168. var allUsers []*state.Session
  169. if screenName := r.PathValue("screenname"); screenName != "" {
  170. session := sessionRetriever.RetrieveByScreenName(state.NewIdentScreenName(screenName))
  171. if session == nil {
  172. http.Error(w, "session not found", http.StatusNotFound)
  173. return
  174. }
  175. allUsers = append(allUsers, session)
  176. } else {
  177. allUsers = sessionRetriever.AllSessions()
  178. }
  179. ou := onlineUsers{
  180. Count: len(allUsers),
  181. Sessions: make([]sessionHandle, len(allUsers)),
  182. }
  183. for i, s := range allUsers {
  184. // report 0 if the user is not idle
  185. idleSeconds := funcTimeSince(s.IdleTime()).Seconds()
  186. if !s.Idle() {
  187. idleSeconds = 0
  188. }
  189. onlineSeconds := funcTimeSince(s.SignonTime()).Seconds()
  190. ou.Sessions[i] = sessionHandle{
  191. ID: s.IdentScreenName().String(),
  192. ScreenName: s.DisplayScreenName().String(),
  193. OnlineSeconds: onlineSeconds,
  194. AwayMessage: s.AwayMessage(),
  195. IdleSeconds: idleSeconds,
  196. IsICQ: s.UIN() > 0,
  197. }
  198. }
  199. if err := json.NewEncoder(w).Encode(ou); err != nil {
  200. http.Error(w, err.Error(), http.StatusInternalServerError)
  201. return
  202. }
  203. }
  204. // getUserHandler handles the GET /user endpoint.
  205. func getUserHandler(w http.ResponseWriter, userManager UserManager, logger *slog.Logger) {
  206. w.Header().Set("Content-Type", "application/json")
  207. users, err := userManager.AllUsers()
  208. if err != nil {
  209. logger.Error("error in GET /user", "err", err.Error())
  210. http.Error(w, "internal server error", http.StatusInternalServerError)
  211. return
  212. }
  213. out := make([]userHandle, len(users))
  214. for i, u := range users {
  215. out[i] = userHandle{
  216. ID: u.IdentScreenName.String(),
  217. ScreenName: u.DisplayScreenName.String(),
  218. IsICQ: u.IsICQ,
  219. }
  220. }
  221. if err := json.NewEncoder(w).Encode(out); err != nil {
  222. http.Error(w, err.Error(), http.StatusInternalServerError)
  223. return
  224. }
  225. }
  226. // postUserHandler handles the POST /user endpoint.
  227. func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  228. input, err := userFromBody(r)
  229. if err != nil {
  230. http.Error(w, err.Error(), http.StatusBadRequest)
  231. return
  232. }
  233. sn := state.DisplayScreenName(input.ScreenName)
  234. if sn.IsUIN() {
  235. if err := sn.ValidateUIN(); err != nil {
  236. http.Error(w, fmt.Sprintf("invalid uin: %s", err), http.StatusBadRequest)
  237. return
  238. }
  239. } else {
  240. if err := sn.ValidateAIMHandle(); err != nil {
  241. http.Error(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  242. return
  243. }
  244. }
  245. user := state.User{
  246. AuthKey: newUUID().String(),
  247. DisplayScreenName: sn,
  248. IdentScreenName: sn.IdentScreenName(),
  249. IsICQ: sn.IsUIN(),
  250. }
  251. if err := user.HashPassword(input.Password); err != nil {
  252. http.Error(w, fmt.Sprintf("invalid password: %s", err), http.StatusBadRequest)
  253. return
  254. }
  255. err = userManager.InsertUser(user)
  256. switch {
  257. case errors.Is(err, state.ErrDupUser):
  258. http.Error(w, "user already exists", http.StatusConflict)
  259. return
  260. case err != nil:
  261. logger.Error("error inserting user POST /user", "err", err.Error())
  262. http.Error(w, "internal server error", http.StatusInternalServerError)
  263. return
  264. }
  265. w.WriteHeader(http.StatusCreated)
  266. _, _ = fmt.Fprintln(w, "User account created successfully.")
  267. }
  268. func userFromBody(r *http.Request) (userWithPassword, error) {
  269. user := userWithPassword{}
  270. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  271. return userWithPassword{}, errors.New("malformed input")
  272. }
  273. return user, nil
  274. }
  275. // getUserLoginHandler is a temporary endpoint for validating user credentials for
  276. // chivanet. do not rely on this endpoint, as it will be eventually removed.
  277. func getUserLoginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  278. authHeader := r.Header.Get("Authorization")
  279. if authHeader == "" {
  280. // No authentication header found
  281. w.WriteHeader(http.StatusUnauthorized)
  282. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  283. _, _ = w.Write([]byte("401 Unauthorized\n"))
  284. return
  285. }
  286. auth := strings.SplitN(authHeader, " ", 2)
  287. if len(auth) != 2 || auth[0] != "Basic" {
  288. w.WriteHeader(http.StatusUnauthorized)
  289. _, _ = w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  290. return
  291. }
  292. payload, err := base64.StdEncoding.DecodeString(auth[1])
  293. if err != nil {
  294. w.WriteHeader(http.StatusUnauthorized)
  295. _, _ = w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  296. return
  297. }
  298. pair := strings.SplitN(string(payload), ":", 2)
  299. if len(pair) != 2 {
  300. w.WriteHeader(http.StatusUnauthorized)
  301. _, _ = w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  302. return
  303. }
  304. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  305. user, err := userManager.User(username)
  306. if err != nil {
  307. w.WriteHeader(http.StatusInternalServerError)
  308. _, _ = w.Write([]byte("500 InternalServerError\n"))
  309. logger.Error("error getting user", "err", err.Error())
  310. return
  311. }
  312. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  313. w.WriteHeader(http.StatusUnauthorized)
  314. _, _ = w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  315. return
  316. }
  317. // Successfully authenticated
  318. w.WriteHeader(http.StatusOK)
  319. _, _ = w.Write([]byte("200 OK: Successfully Authenticated\n"))
  320. }
  321. // getPublicChatHandler handles the GET /chat/room/public endpoint.
  322. func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  323. w.Header().Set("Content-Type", "application/json")
  324. rooms, err := chatRoomRetriever.AllChatRooms(state.PublicExchange)
  325. if err != nil {
  326. logger.Error("error in GET /chat/rooms/public", "err", err.Error())
  327. http.Error(w, "internal server error", http.StatusInternalServerError)
  328. return
  329. }
  330. out := make([]chatRoom, len(rooms))
  331. for i, room := range rooms {
  332. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  333. cr := chatRoom{
  334. CreateTime: room.CreateTime(),
  335. Name: room.Name(),
  336. Participants: make([]aimChatUserHandle, len(sessions)),
  337. URL: room.URL().String(),
  338. }
  339. for j, sess := range sessions {
  340. cr.Participants[j] = aimChatUserHandle{
  341. ID: sess.IdentScreenName().String(),
  342. ScreenName: sess.DisplayScreenName().String(),
  343. }
  344. }
  345. out[i] = cr
  346. }
  347. if err := json.NewEncoder(w).Encode(out); err != nil {
  348. http.Error(w, err.Error(), http.StatusInternalServerError)
  349. return
  350. }
  351. }
  352. // postPublicChatHandler handles the POST /chat/room/public endpoint.
  353. func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, logger *slog.Logger) {
  354. input := chatRoomCreate{}
  355. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  356. http.Error(w, "invalid input", http.StatusBadRequest)
  357. return
  358. }
  359. input.Name = strings.TrimSpace(input.Name)
  360. if input.Name == "" || len(input.Name) > 50 {
  361. http.Error(w, "chat room name must be between 1 and 50 characters", http.StatusBadRequest)
  362. return
  363. }
  364. cr := state.NewChatRoom(input.Name, state.NewIdentScreenName("system"), state.PublicExchange)
  365. err := chatRoomCreator.CreateChatRoom(&cr)
  366. switch {
  367. case errors.Is(err, state.ErrDupChatRoom):
  368. http.Error(w, "Chat room already exists.", http.StatusConflict)
  369. return
  370. case err != nil:
  371. logger.Error("error inserting chat room POST /chat/room/public", "err", err.Error())
  372. http.Error(w, "internal server error", http.StatusInternalServerError)
  373. return
  374. }
  375. w.WriteHeader(http.StatusCreated)
  376. _, _ = fmt.Fprintln(w, "Chat room created successfully.")
  377. }
  378. // getPrivateChatHandler handles the GET /chat/room/private endpoint.
  379. func getPrivateChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  380. w.Header().Set("Content-Type", "application/json")
  381. rooms, err := chatRoomRetriever.AllChatRooms(state.PrivateExchange)
  382. if err != nil {
  383. logger.Error("error in GET /chat/rooms/private", "err", err.Error())
  384. http.Error(w, "internal server error", http.StatusInternalServerError)
  385. return
  386. }
  387. out := make([]chatRoom, len(rooms))
  388. for i, room := range rooms {
  389. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  390. cr := chatRoom{
  391. CreateTime: room.CreateTime(),
  392. CreatorID: room.Creator().String(),
  393. Name: room.Name(),
  394. Participants: make([]aimChatUserHandle, len(sessions)),
  395. URL: room.URL().String(),
  396. }
  397. for j, sess := range sessions {
  398. cr.Participants[j] = aimChatUserHandle{
  399. ID: sess.IdentScreenName().String(),
  400. ScreenName: sess.DisplayScreenName().String(),
  401. }
  402. }
  403. out[i] = cr
  404. }
  405. if err := json.NewEncoder(w).Encode(out); err != nil {
  406. http.Error(w, err.Error(), http.StatusInternalServerError)
  407. return
  408. }
  409. }
  410. // postIMHandler handles the POST /instant-message endpoint.
  411. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  412. input := instantMessage{}
  413. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  414. http.Error(w, "malformed input", http.StatusBadRequest)
  415. return
  416. }
  417. tlv, err := wire.ICBMFragmentList(input.Text)
  418. if err != nil {
  419. logger.Error("error sending message POST /instant-message", "err", err.Error())
  420. http.Error(w, "internal server error", http.StatusInternalServerError)
  421. return
  422. }
  423. msg := wire.SNACMessage{
  424. Frame: wire.SNACFrame{
  425. FoodGroup: wire.ICBM,
  426. SubGroup: wire.ICBMChannelMsgToClient,
  427. },
  428. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  429. ChannelID: 1,
  430. TLVUserInfo: wire.TLVUserInfo{
  431. ScreenName: input.From,
  432. },
  433. TLVRestBlock: wire.TLVRestBlock{
  434. TLVList: wire.TLVList{
  435. wire.NewTLVBE(wire.ICBMTLVAOLIMData, tlv),
  436. },
  437. },
  438. },
  439. }
  440. messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
  441. w.WriteHeader(http.StatusOK)
  442. _, _ = fmt.Fprintln(w, "Message sent successfully.")
  443. }
  444. // getUserBuddyIconHandler handles the GET /user/{screenname}/icon endpoint.
  445. func getUserBuddyIconHandler(w http.ResponseWriter, r *http.Request, u UserManager, f FeedBagRetriever, b BARTRetriever, logger *slog.Logger) {
  446. screenName := state.NewIdentScreenName(r.PathValue("screenname"))
  447. user, err := u.User(screenName)
  448. if err != nil {
  449. logger.Error("error retrieving user", "err", err.Error())
  450. http.Error(w, "internal server error", http.StatusInternalServerError)
  451. return
  452. }
  453. if user == nil {
  454. http.Error(w, "user not found", http.StatusNotFound)
  455. return
  456. }
  457. iconRef, err := f.BuddyIconRefByName(screenName)
  458. if err != nil {
  459. logger.Error("error retrieving buddy icon ref", "err", err.Error())
  460. http.Error(w, "internal server error", http.StatusInternalServerError)
  461. return
  462. }
  463. if iconRef == nil || iconRef.HasClearIconHash() {
  464. http.Error(w, "icon not found", http.StatusNotFound)
  465. return
  466. }
  467. icon, err := b.BARTRetrieve(iconRef.Hash)
  468. if err != nil {
  469. logger.Error("error retrieving buddy icon bart item", "err", err.Error())
  470. http.Error(w, "internal server error", http.StatusInternalServerError)
  471. return
  472. }
  473. w.Header().Set("Content-Type", http.DetectContentType(icon))
  474. w.Write(icon)
  475. }
  476. // getUserAccountHandler handles the GET /user/{screenname}/account endpoint.
  477. func getUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountRetriever, p ProfileRetriever, logger *slog.Logger) {
  478. w.Header().Set("Content-Type", "application/json")
  479. screenName := r.PathValue("screenname")
  480. user, err := userManager.User(state.NewIdentScreenName(screenName))
  481. if err != nil {
  482. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  483. http.Error(w, "internal server error", http.StatusInternalServerError)
  484. return
  485. }
  486. if user == nil {
  487. http.Error(w, "user not found", http.StatusNotFound)
  488. return
  489. }
  490. emailAddress := ""
  491. email, err := a.EmailAddressByName(user.IdentScreenName)
  492. if err != nil {
  493. emailAddress = ""
  494. } else {
  495. emailAddress = email.String()
  496. }
  497. regStatus, err := a.RegStatusByName(user.IdentScreenName)
  498. if err != nil {
  499. logger.Error("error in GET /user/*/account RegStatus", "err", err.Error())
  500. http.Error(w, "internal server error", http.StatusInternalServerError)
  501. return
  502. }
  503. confirmStatus, err := a.ConfirmStatusByName(user.IdentScreenName)
  504. if err != nil {
  505. logger.Error("error in GET /user/*/account ConfirmStatus", "err", err.Error())
  506. http.Error(w, "internal server error", http.StatusInternalServerError)
  507. return
  508. }
  509. profile, err := p.Profile(user.IdentScreenName)
  510. if err != nil {
  511. logger.Error("error in GET /user/*/account Profile", "err", err.Error())
  512. http.Error(w, "internal server error", http.StatusInternalServerError)
  513. return
  514. }
  515. out := userAccountHandle{
  516. ID: user.IdentScreenName.String(),
  517. ScreenName: user.DisplayScreenName.String(),
  518. EmailAddress: emailAddress,
  519. RegStatus: regStatus,
  520. Confirmed: confirmStatus,
  521. Profile: profile,
  522. IsICQ: user.IsICQ,
  523. }
  524. if err := json.NewEncoder(w).Encode(out); err != nil {
  525. http.Error(w, err.Error(), http.StatusInternalServerError)
  526. return
  527. }
  528. }
  529. // getVersionHandler handles the GET /version endpoint.
  530. func getVersionHandler(w http.ResponseWriter, bld config.Build) {
  531. w.Header().Set("Content-Type", "application/json")
  532. if err := json.NewEncoder(w).Encode(bld); err != nil {
  533. http.Error(w, err.Error(), http.StatusInternalServerError)
  534. return
  535. }
  536. }
  537. // getDirectoryCategoryHandler handles the GET /directory/category endpoint.
  538. func getDirectoryCategoryHandler(w http.ResponseWriter, manager DirectoryManager, logger *slog.Logger) {
  539. w.Header().Set("Content-Type", "application/json")
  540. categories, err := manager.Categories()
  541. if err != nil {
  542. logger.Error("error in GET /directory/category", "err", err.Error())
  543. errorMsg(w, "internal server error", http.StatusInternalServerError)
  544. return
  545. }
  546. out := make([]directoryCategory, len(categories))
  547. for i, category := range categories {
  548. out[i] = directoryCategory{
  549. ID: category.ID,
  550. Name: category.Name,
  551. }
  552. }
  553. if err := json.NewEncoder(w).Encode(out); err != nil {
  554. errorMsg(w, err.Error(), http.StatusInternalServerError)
  555. }
  556. }
  557. // postDirectoryCategoryHandler handles the POST /directory/category endpoint.
  558. func postDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  559. input := directoryCategoryCreate{}
  560. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  561. errorMsg(w, "malformed input", http.StatusBadRequest)
  562. return
  563. }
  564. category, err := manager.CreateCategory(input.Name)
  565. if err != nil {
  566. if errors.Is(err, state.ErrKeywordCategoryExists) {
  567. errorMsg(w, "category already exists", http.StatusConflict)
  568. } else {
  569. logger.Error("error in POST /directory/category", "err", err.Error())
  570. errorMsg(w, "internal server error", http.StatusInternalServerError)
  571. }
  572. return
  573. }
  574. w.WriteHeader(http.StatusCreated)
  575. dc := directoryCategory{
  576. ID: category.ID,
  577. Name: category.Name,
  578. }
  579. if err := json.NewEncoder(w).Encode(dc); err != nil {
  580. errorMsg(w, err.Error(), http.StatusBadRequest)
  581. }
  582. }
  583. // deleteDirectoryCategoryHandler handles the DELETE /directory/category/{id} endpoint.
  584. func deleteDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  585. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  586. if err != nil {
  587. http.Error(w, "invalid category ID", http.StatusBadRequest)
  588. return
  589. }
  590. if err := manager.DeleteCategory(uint8(categoryID)); err != nil {
  591. switch {
  592. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  593. errorMsg(w, "category not found", http.StatusNotFound)
  594. return
  595. case errors.Is(err, state.ErrKeywordInUse):
  596. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  597. return
  598. default:
  599. logger.Error("error in DELETE /directory/category/{id}", "err", err.Error())
  600. errorMsg(w, "internal server error", http.StatusInternalServerError)
  601. return
  602. }
  603. }
  604. w.WriteHeader(http.StatusNoContent)
  605. }
  606. // getDirectoryCategoryKeywordHandler handles the GET /directory/category/{id}/keyword endpoint.
  607. func getDirectoryCategoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  608. w.Header().Set("Content-Type", "application/json")
  609. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  610. if err != nil {
  611. errorMsg(w, "invalid category ID", http.StatusBadRequest)
  612. return
  613. }
  614. categories, err := manager.KeywordsByCategory(uint8(categoryID))
  615. if err != nil {
  616. if errors.Is(err, state.ErrKeywordCategoryNotFound) {
  617. errorMsg(w, "category not found", http.StatusNotFound)
  618. } else {
  619. logger.Error("error in GET /directory/category/{id}/keyword", "err", err.Error())
  620. errorMsg(w, "internal server error", http.StatusInternalServerError)
  621. }
  622. return
  623. }
  624. out := make([]directoryCategory, len(categories))
  625. for i, category := range categories {
  626. out[i] = directoryCategory{
  627. ID: category.ID,
  628. Name: category.Name,
  629. }
  630. }
  631. if err := json.NewEncoder(w).Encode(out); err != nil {
  632. errorMsg(w, err.Error(), http.StatusInternalServerError)
  633. }
  634. }
  635. // postDirectoryKeywordHandler handles the POST /directory/keyword endpoint.
  636. func postDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  637. w.Header().Set("Content-Type", "application/json")
  638. input := directoryKeywordCreate{}
  639. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  640. errorMsg(w, "malformed input", http.StatusBadRequest)
  641. return
  642. }
  643. kw, err := manager.CreateKeyword(input.Name, input.CategoryID)
  644. if err != nil {
  645. switch {
  646. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  647. errorMsg(w, "category not found", http.StatusNotFound)
  648. return
  649. case errors.Is(err, state.ErrKeywordExists):
  650. errorMsg(w, "keyword already exists", http.StatusConflict)
  651. return
  652. default:
  653. logger.Error("error in POST /directory/keyword", "err", err.Error())
  654. errorMsg(w, "internal server error", http.StatusInternalServerError)
  655. return
  656. }
  657. }
  658. w.WriteHeader(http.StatusCreated)
  659. dc := directoryKeyword{
  660. ID: kw.ID,
  661. Name: kw.Name,
  662. }
  663. if err := json.NewEncoder(w).Encode(dc); err != nil {
  664. errorMsg(w, err.Error(), http.StatusBadRequest)
  665. }
  666. }
  667. // deleteDirectoryKeywordHandler handles the DELETE /directory/keyword/{id} endpoint.
  668. func deleteDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  669. keywordID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  670. if err != nil {
  671. errorMsg(w, "invalid keyword ID", http.StatusBadRequest)
  672. return
  673. }
  674. if err := manager.DeleteKeyword(uint8(keywordID)); err != nil {
  675. switch {
  676. case errors.Is(err, state.ErrKeywordInUse):
  677. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  678. return
  679. case errors.Is(err, state.ErrKeywordNotFound):
  680. errorMsg(w, "keyword not found", http.StatusNotFound)
  681. return
  682. default:
  683. logger.Error("error in DELETE /directory/keyword/{id}", "err", err.Error())
  684. errorMsg(w, "internal server error", http.StatusInternalServerError)
  685. return
  686. }
  687. }
  688. w.WriteHeader(http.StatusNoContent)
  689. }
  690. // errorMsg sends an error response message and code.
  691. func errorMsg(w http.ResponseWriter, error string, code int) {
  692. msg := messageBody{Message: error}
  693. w.WriteHeader(code)
  694. if err := json.NewEncoder(w).Encode(msg); err != nil {
  695. http.Error(w, err.Error(), http.StatusInternalServerError)
  696. }
  697. }