admin_handlers.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. package controller
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "git.aetherial.dev/aeth/keiji/pkg/auth"
  12. "git.aetherial.dev/aeth/keiji/pkg/storage"
  13. "github.com/gin-gonic/gin"
  14. )
  15. const AUTH_COOKIE_NAME = "X-Server-Auth"
  16. // adminTables and adminImages preserve compatibility with legacy repositories.
  17. func (c *Controller) adminTables() (storage.AdminPage, error) {
  18. if reader, ok := c.database.(storage.DocumentListReader); ok {
  19. return reader.GetAdminTablesWithError()
  20. }
  21. return c.database.GetAdminTables(), nil
  22. }
  23. func (c *Controller) adminImages() ([]storage.Image, error) {
  24. if reader, ok := c.database.(storage.DocumentListReader); ok {
  25. return reader.GetAllImagesWithError()
  26. }
  27. return c.database.GetAllImages(), nil
  28. }
  29. // @Name ServeLogin
  30. // @Summary serves the HTML login page
  31. // @Tags admin
  32. // @Router /login [get]
  33. func (c *Controller) ServeLogin(ctx *gin.Context) {
  34. cookie, _ := ctx.Cookie(AUTH_COOKIE_NAME)
  35. if c.Cache.Read(cookie) {
  36. ctx.Redirect(302, "/home")
  37. }
  38. ctx.HTML(http.StatusOK, "login", gin.H{
  39. "heading": "aetherial.dev login",
  40. })
  41. }
  42. // @Name Auth
  43. // @Summary serves recieves admin user and pass, sets a cookie
  44. // @Tags admin
  45. // @Param cred body storage.Credentials true "Admin Credentials"
  46. // @Router /login [post]
  47. func (c *Controller) Auth(ctx *gin.Context) {
  48. var cred auth.Credentials
  49. err := ctx.ShouldBind(&cred)
  50. if err != nil {
  51. ctx.JSON(400, map[string]string{
  52. "Error": err.Error(),
  53. })
  54. return
  55. }
  56. cookie, err := auth.Authorize(&cred, c.Cache, c.AuthSource)
  57. if err != nil {
  58. ctx.JSON(400, map[string]string{
  59. "Error": err.Error(),
  60. })
  61. return
  62. }
  63. page, err := c.adminTables()
  64. if err != nil {
  65. htmlListError(ctx, err)
  66. return
  67. }
  68. headers, menu, err := c.htmlNavigation()
  69. if err != nil {
  70. htmlListError(ctx, err)
  71. return
  72. }
  73. ctx.SetSameSite(http.SameSiteLaxMode)
  74. ctx.SetCookie(AUTH_COOKIE_NAME, cookie, 1200, "/", "", !insecureCookies(), true)
  75. tables := page.Tables
  76. if tables == nil {
  77. tables = map[string][]storage.TableData{}
  78. }
  79. tables["modify"] = append(tables["modify"], storage.TableData{DisplayName: "Digital Art", Link: "/admin/digital"})
  80. ctx.HTML(http.StatusOK, "admin", gin.H{
  81. "navigation": gin.H{
  82. "headers": headers,
  83. "menu": menu,
  84. },
  85. "Tables": tables,
  86. })
  87. }
  88. /*
  89. @Name AddAdminTableEntry
  90. @Summary add an entry to the admin table
  91. @Tags admin
  92. @Router /admin/panel
  93. */
  94. func (c *Controller) AddAdminTableEntry(ctx *gin.Context) {
  95. tables := make(map[string][]storage.TableData)
  96. adminPage := storage.AdminPage{Tables: tables}
  97. err := ctx.ShouldBind(&adminPage)
  98. if err != nil {
  99. ctx.JSON(400, map[string]string{
  100. "Error": err.Error(),
  101. })
  102. return
  103. }
  104. for category := range adminPage.Tables {
  105. for entry := range adminPage.Tables[category] {
  106. err := c.database.AddAdminTableEntry(adminPage.Tables[category][entry], category)
  107. if err != nil {
  108. ctx.JSON(400, map[string]string{
  109. "Error": err.Error(),
  110. })
  111. return
  112. }
  113. }
  114. }
  115. ctx.Data(200, "text", []byte("Categories populated."))
  116. }
  117. /*
  118. @Name AddMenuItem
  119. @Summary add an entry to the sidebar menu
  120. @Tags admin
  121. @Router /admin/menu
  122. */
  123. func (c *Controller) AddMenuItem(ctx *gin.Context) {
  124. var item storage.LinkPair
  125. err := ctx.ShouldBind(&item)
  126. if err != nil {
  127. ctx.JSON(400, map[string]string{
  128. "Error": err.Error(),
  129. })
  130. return
  131. }
  132. err = c.database.AddMenuItem(item)
  133. if err != nil {
  134. ctx.JSON(400, map[string]string{
  135. "Error": err.Error(),
  136. })
  137. return
  138. }
  139. ctx.Data(200, "text", []byte("menu item added."))
  140. }
  141. /*
  142. @Name AddNavbarItem
  143. @Summary add an entry to the navbar
  144. @Tags admin
  145. @Router /admin/navbar
  146. */
  147. func (c *Controller) AddNavbarItem(ctx *gin.Context) {
  148. file, err := ctx.FormFile("file")
  149. if err != nil {
  150. ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
  151. return
  152. }
  153. fh, err := file.Open()
  154. if err != nil {
  155. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
  156. return
  157. }
  158. fb := make([]byte, file.Size)
  159. var output bytes.Buffer
  160. for {
  161. n, err := fh.Read(fb)
  162. if err == io.EOF {
  163. break
  164. }
  165. if err != nil {
  166. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
  167. return
  168. }
  169. output.Write(fb[:n])
  170. }
  171. var item storage.NavBarItem
  172. item = storage.NavBarItem{
  173. Link: file.Filename,
  174. Png: fb,
  175. }
  176. err = ctx.ShouldBind(&item)
  177. if err != nil {
  178. ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
  179. return
  180. }
  181. err = c.database.AddNavbarItem(item)
  182. if err != nil {
  183. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
  184. return
  185. }
  186. err = c.database.AddAsset(item.File.Filename, fb)
  187. if err != nil {
  188. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
  189. return
  190. }
  191. ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Addition Successful!", "Color": "green"})
  192. }
  193. /*
  194. @Name RemoveNavbarItem
  195. @Summary remove an entry from the navbar
  196. @Tags admin
  197. @Router /admin/navbar
  198. */
  199. func (c *Controller) RemoveNavbarItem(ctx *gin.Context) {
  200. itemName, ok := ctx.Params.Get("item_name")
  201. if !ok {
  202. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": errors.New("No navbar name passed."), "Color": "red"})
  203. return
  204. }
  205. err := c.database.DeleteNavbarItem(storage.Identifier(itemName))
  206. if err != nil {
  207. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
  208. return
  209. }
  210. ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Deleted '" + itemName + "' Successfully!", "Color": "green"})
  211. }
  212. func (c *Controller) ServeNavbarModify(ctx *gin.Context) {
  213. navbar, menu, err := c.htmlNavigation()
  214. if err != nil {
  215. htmlListError(ctx, err)
  216. return
  217. }
  218. tableData := storage.AdminPage{Tables: map[string][]storage.TableData{}}
  219. for i := range navbar {
  220. tableData.Tables["items"] = append(tableData.Tables["items"],
  221. storage.TableData{
  222. DisplayName: navbar[i].Link,
  223. Link: fmt.Sprintf("/admin/navbar/options/%s", navbar[i].Link),
  224. },
  225. )
  226. }
  227. ctx.HTML(200, "admin", gin.H{
  228. "navigation": gin.H{
  229. "menu": menu,
  230. "headers": navbar,
  231. },
  232. "Tables": tableData.Tables,
  233. })
  234. }
  235. /*
  236. @Name AddAsset
  237. @Summary add an asset to the db
  238. @Tags admin
  239. @Router /admin/assets
  240. */
  241. func (c *Controller) AddAsset(ctx *gin.Context) {
  242. var item storage.Asset
  243. err := ctx.ShouldBind(&item)
  244. if err != nil {
  245. ctx.JSON(400, map[string]string{
  246. "Error": err.Error(),
  247. })
  248. return
  249. }
  250. err = c.database.AddAsset(item.Name, item.Data)
  251. if err != nil {
  252. ctx.JSON(400, map[string]string{
  253. "Error": err.Error(),
  254. })
  255. return
  256. }
  257. }
  258. // @Name AdminPanel
  259. // @Summary serve the admin panel page
  260. // @Tags admin
  261. // @Router /admin/panel [get]
  262. func (c *Controller) AdminPanel(ctx *gin.Context) {
  263. page, err := c.adminTables()
  264. if err != nil {
  265. htmlListError(ctx, err)
  266. return
  267. }
  268. headers, menu, err := c.htmlNavigation()
  269. if err != nil {
  270. htmlListError(ctx, err)
  271. return
  272. }
  273. tables := page.Tables
  274. if tables == nil {
  275. tables = map[string][]storage.TableData{}
  276. }
  277. tables["modify"] = append(tables["modify"], storage.TableData{DisplayName: "Digital Art", Link: "/admin/digital"})
  278. ctx.HTML(http.StatusOK, "admin", gin.H{
  279. "navigation": gin.H{
  280. "headers": headers,
  281. "menu": menu,
  282. },
  283. "Tables": tables,
  284. })
  285. }
  286. /*
  287. Serves the admin panel with all of the documents in each blog category for editing
  288. */
  289. func (c *Controller) ServeBlogDirectory(ctx *gin.Context) {
  290. tableData := storage.AdminPage{Tables: map[string][]storage.TableData{}}
  291. for i := range storage.Topics {
  292. docs, err := c.htmlDocuments(storage.Topics[i])
  293. if err != nil {
  294. htmlListError(ctx, err)
  295. return
  296. }
  297. for z := range docs {
  298. tableData.Tables[storage.Topics[i]] = append(tableData.Tables[storage.Topics[i]],
  299. storage.TableData{
  300. DisplayName: docs[z].Title,
  301. Link: fmt.Sprintf("/admin/options/%s", docs[z].Ident),
  302. },
  303. )
  304. }
  305. }
  306. headers, menu, err := c.htmlNavigation()
  307. if err != nil {
  308. htmlListError(ctx, err)
  309. return
  310. }
  311. ctx.HTML(200, "admin", gin.H{
  312. "navigation": gin.H{
  313. "menu": menu,
  314. "headers": headers,
  315. },
  316. "Tables": tableData.Tables,
  317. })
  318. }
  319. /*
  320. Serves the blogpost editor with the submit button set to PATCH a document
  321. */
  322. func (c *Controller) GetBlogPostEditor(ctx *gin.Context) {
  323. post, exist := ctx.Params.Get("id")
  324. if !exist {
  325. ctx.JSON(404, map[string]string{
  326. "Error": "the requested file could not be found",
  327. })
  328. return
  329. }
  330. doc, err := c.database.GetDocument(storage.Identifier(post))
  331. if err != nil {
  332. ctx.JSON(500, map[string]string{
  333. "Error": err.Error(),
  334. })
  335. return
  336. }
  337. headers, menu, err := c.htmlNavigation()
  338. if err != nil {
  339. htmlListError(ctx, err)
  340. return
  341. }
  342. images, err := c.adminImages()
  343. if err != nil {
  344. htmlListError(ctx, err)
  345. return
  346. }
  347. ctx.HTML(200, "blogpost_editor", gin.H{
  348. "navigation": gin.H{
  349. "menu": menu,
  350. "headers": headers,
  351. },
  352. "Ident": doc.Ident,
  353. "Topics": storage.Topics,
  354. "Title": doc.Title,
  355. "DefaultTopic": doc.Category,
  356. "Created": doc.Created,
  357. "Body": doc.Body,
  358. "Images": images,
  359. })
  360. }
  361. /*
  362. Update an existing blog post
  363. */
  364. func (c *Controller) UpdateBlogPost(ctx *gin.Context) {
  365. var doc storage.Document
  366. err := ctx.ShouldBind(&doc)
  367. if err != nil {
  368. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": "Update Failed!", "Color": "red"})
  369. return
  370. }
  371. err = c.database.UpdateDocument(doc)
  372. if err != nil {
  373. ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": "Update Failed!", "Color": "red"})
  374. return
  375. }
  376. ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Update Successful!", "Color": "green"})
  377. }
  378. /*
  379. Serving the new blogpost page. Serves the editor with the method to POST a new document
  380. */
  381. func (c *Controller) ServeNewBlogPage(ctx *gin.Context) {
  382. headers, menu, err := c.htmlNavigation()
  383. if err != nil {
  384. htmlListError(ctx, err)
  385. return
  386. }
  387. images, err := c.adminImages()
  388. if err != nil {
  389. htmlListError(ctx, err)
  390. return
  391. }
  392. ctx.HTML(200, "blogpost_editor", gin.H{
  393. "navigation": gin.H{
  394. "menu": menu,
  395. "headers": headers,
  396. },
  397. "Post": true,
  398. "Images": images,
  399. "Topics": storage.Topics,
  400. "Created": time.Now().UTC().String(),
  401. })
  402. }
  403. /*
  404. Serving the new blogpost page. Serves the editor with the method to POST a new document
  405. */
  406. func (c *Controller) ServeNewNavbarItem(ctx *gin.Context) {
  407. headers, menu, err := c.htmlNavigation()
  408. if err != nil {
  409. htmlListError(ctx, err)
  410. return
  411. }
  412. ctx.HTML(200, "navbar_editor", gin.H{
  413. "navigation": gin.H{
  414. "menu": menu,
  415. "headers": headers,
  416. },
  417. "Post": true,
  418. })
  419. }
  420. /*
  421. Reciever for the ServeNewBlogPage UI screen. Adds a new document to the database
  422. */
  423. func (c *Controller) MakeBlogPost(ctx *gin.Context) {
  424. var doc storage.Document
  425. err := ctx.ShouldBind(&doc)
  426. if err != nil {
  427. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": "Update Failed!", "Color": "red"})
  428. return
  429. }
  430. _, err = c.database.AddDocument(doc)
  431. if err != nil {
  432. ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": "Update Failed!", "Color": "red"})
  433. return
  434. }
  435. ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Update Successful!", "Color": "green"})
  436. }
  437. /*
  438. Serves the HTML page for a new visual media post
  439. */
  440. func (c *Controller) ServeFileUpload(ctx *gin.Context) {
  441. headers, menu, err := c.htmlNavigation()
  442. if err != nil {
  443. htmlListError(ctx, err)
  444. return
  445. }
  446. ctx.HTML(200, "upload", gin.H{
  447. "navigation": gin.H{
  448. "menu": menu,
  449. "headers": headers,
  450. },
  451. "Topics": storage.ImageCategories,
  452. "DefaultTopic": storage.DIGITAL_ART,
  453. })
  454. }
  455. /*
  456. Reciever for the page served to created a new visual media post
  457. */
  458. // maxUploadSize limits the file itself; the request has additional room for form fields.
  459. const maxUploadSize = 32 << 20
  460. const maxUploadRequestSize = maxUploadSize + (1 << 20)
  461. func (c *Controller) SaveFile(ctx *gin.Context) {
  462. ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxUploadRequestSize)
  463. defer func() {
  464. if ctx.Request.MultipartForm != nil {
  465. ctx.Request.MultipartForm.RemoveAll()
  466. }
  467. }()
  468. fail := func(status int, message string) {
  469. ctx.HTML(status, "upload_status", gin.H{"UpdateMessage": message, "Color": "red"})
  470. }
  471. var img storage.Image
  472. if err := ctx.ShouldBind(&img); err != nil {
  473. var maxErr *http.MaxBytesError
  474. if errors.As(err, &maxErr) {
  475. fail(http.StatusRequestEntityTooLarge, "Upload too large")
  476. } else {
  477. fail(http.StatusBadRequest, "Invalid upload")
  478. }
  479. return
  480. }
  481. file, err := ctx.FormFile("file")
  482. if err != nil {
  483. fail(http.StatusBadRequest, "Missing upload file")
  484. return
  485. }
  486. if file.Size > maxUploadSize {
  487. fail(http.StatusRequestEntityTooLarge, "Upload too large")
  488. return
  489. }
  490. fh, err := file.Open()
  491. if err != nil {
  492. fail(http.StatusInternalServerError, "Could not open upload")
  493. return
  494. }
  495. defer fh.Close()
  496. data, err := io.ReadAll(io.LimitReader(fh, maxUploadSize+1))
  497. if err != nil {
  498. fail(http.StatusInternalServerError, "Could not read upload")
  499. return
  500. }
  501. if len(data) > maxUploadSize {
  502. fail(http.StatusRequestEntityTooLarge, "Upload too large")
  503. return
  504. }
  505. mediaType := detectMediaType(data)
  506. if mediaType == "" {
  507. fail(http.StatusUnsupportedMediaType, "Unsupported media type")
  508. return
  509. }
  510. if _, err = c.database.AddImage(data, img.Title, img.Desc, img.Category, mediaType); err != nil {
  511. fail(http.StatusInternalServerError, "Could not save upload")
  512. return
  513. }
  514. ctx.HTML(http.StatusOK, "upload_status", gin.H{"UpdateMessage": "Update Successful!", "Color": "green"})
  515. }
  516. // Serve the document deletion template
  517. func (c *Controller) NavbarOptions(ctx *gin.Context) {
  518. id, found := ctx.Params.Get("id")
  519. if !found {
  520. ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": "No ID selected!", "Color": "red"})
  521. return
  522. }
  523. ctx.HTML(200, "post_options", gin.H{
  524. "Link": fmt.Sprintf("/admin/navbar/%s", id),
  525. })
  526. }
  527. // Serve the document deletion template
  528. func (c *Controller) PostOptions(ctx *gin.Context) {
  529. id, found := ctx.Params.Get("id")
  530. if !found {
  531. ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": "No ID selected!", "Color": "red"})
  532. return
  533. }
  534. ctx.HTML(200, "post_options", gin.H{
  535. "Link": fmt.Sprintf("/admin/posts/%s", id),
  536. })
  537. }
  538. /*
  539. Delete a document from the database
  540. */
  541. func (c *Controller) DeleteDocument(ctx *gin.Context) {
  542. id, found := ctx.Params.Get("id")
  543. if !found {
  544. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": "No ID passed!", "Color": "red"})
  545. return
  546. }
  547. err := c.database.DeleteDocument(storage.Identifier(id))
  548. if err != nil {
  549. ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": "Delete Failed!", "Color": "red"})
  550. return
  551. }
  552. ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Delete Successful!", "Color": "green"})
  553. }
  554. // Digital art administration is restricted by the /admin authentication middleware.
  555. func (c *Controller) ListDigitalArt(ctx *gin.Context) {
  556. images, err := c.htmlImages(storage.DIGITAL_ART)
  557. if err != nil {
  558. htmlListError(ctx, err)
  559. return
  560. }
  561. ctx.HTML(http.StatusOK, "digital_art_admin_list", gin.H{"Images": images})
  562. }
  563. func (c *Controller) EditDigitalArt(ctx *gin.Context) {
  564. image, err := c.database.GetImage(storage.Identifier(ctx.Param("id")))
  565. if err != nil || image.Category != storage.DIGITAL_ART {
  566. ctx.String(http.StatusNotFound, "Digital art not found")
  567. return
  568. }
  569. ctx.HTML(http.StatusOK, "digital_art_admin_editor", image)
  570. }
  571. func (c *Controller) UpdateDigitalArt(ctx *gin.Context) {
  572. title := strings.TrimSpace(ctx.PostForm("title"))
  573. desc := ctx.PostForm("description")
  574. if title == "" {
  575. ctx.HTML(http.StatusBadRequest, "upload_status", gin.H{"UpdateMessage": "Title is required", "Color": "red"})
  576. return
  577. }
  578. err := c.database.UpdateDigitalArt(storage.Identifier(ctx.Param("id")), title, desc)
  579. if err != nil {
  580. status := http.StatusInternalServerError
  581. message := "Could not update digital art"
  582. if errors.Is(err, storage.ErrNotExists) {
  583. status = http.StatusNotFound
  584. message = "Digital art not found"
  585. } else {
  586. log.Printf("update digital art failed: %v", err)
  587. }
  588. ctx.HTML(status, "upload_status", gin.H{"UpdateMessage": message, "Color": "red"})
  589. return
  590. }
  591. ctx.HTML(http.StatusOK, "upload_status", gin.H{"UpdateMessage": "Digital art updated", "Color": "green"})
  592. }
  593. // RemoveDigitalArt removes the gallery listing while preserving referenced media.
  594. func (c *Controller) RemoveDigitalArt(ctx *gin.Context) {
  595. err := c.database.ArchiveDigitalArt(storage.Identifier(ctx.Param("id")))
  596. if err != nil {
  597. status := http.StatusInternalServerError
  598. message := "Could not remove digital art listing"
  599. if errors.Is(err, storage.ErrNotExists) {
  600. status = http.StatusNotFound
  601. message = "Digital art not found"
  602. } else {
  603. log.Printf("remove digital art listing failed: %v", err)
  604. }
  605. ctx.HTML(status, "upload_status", gin.H{"UpdateMessage": message, "Color": "red"})
  606. return
  607. }
  608. ctx.HTML(http.StatusOK, "upload_status", gin.H{"UpdateMessage": "Listing removed. The media file remains available for posts.", "Color": "green"})
  609. }