chore(router,service): remove editor and projects endpoint and services

They will be reimplemented later
This commit is contained in:
Guz
2025-06-06 16:30:50 -03:00
parent 12844eafee
commit 06807b0623
5 changed files with 0 additions and 878 deletions

View File

@@ -1,283 +0,0 @@
package router
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
"forge.capytal.company/capytalcode/project-comicverse/internals/randstr"
"forge.capytal.company/capytalcode/project-comicverse/service"
"forge.capytal.company/loreddev/x/smalltrip/exception"
)
func (router *router) pages(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
// TODO: Check if project exists
id := r.PathValue("ID")
if id == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
ServeHTTP(w, r)
return
}
pageID := r.PathValue("PageID")
switch getMethod(r) {
case http.MethodGet, http.MethodHead:
if pageID == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "PageID" must be provided`)).
ServeHTTP(w, r)
return
}
router.getPage(w, r)
case http.MethodPost:
router.addPage(w, r)
case http.MethodDelete:
if pageID == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "PageID" must be provided`)).
ServeHTTP(w, r)
return
}
router.deletePage(w, r)
default:
exception.
MethodNotAllowed([]string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodDelete,
}).
ServeHTTP(w, r)
}
}
func (router *router) addPage(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
id := r.PathValue("ID")
router.assert.NotZero(id, "This method should be used after the path values are checked")
img, _, err := r.FormFile("image")
if err != nil {
// TODO: Handle if the file is bigger than allowed by ParseForm (10mb)
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
err = router.service.AddPage(id, img)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther)
}
func (router *router) getPage(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
id := r.PathValue("ID")
router.assert.NotZero(id, "This method should be used after the path values are checked")
pageID := r.PathValue("PageID")
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
page, err := router.service.GetPage(id, pageID)
if errors.Is(err, service.ErrPageNotExists) {
exception.NotFound(exception.WithError(err)).ServeHTTP(w, r)
return
}
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
if i, ok := page.Image.(io.WriterTo); ok {
_, err = i.WriteTo(w)
} else {
_, err = io.Copy(w, page.Image)
}
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
}
func (router *router) deletePage(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
id := r.PathValue("ID")
router.assert.NotZero(id, "This method should be used after the path values are checked")
pageID := r.PathValue("PageID")
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
err := router.service.DeletePage(id, pageID)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther)
}
func (router *router) interactions(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
// TODO: Check if the project exists
id := r.PathValue("ID")
if id == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
ServeHTTP(w, r)
return
}
// TODO: Check if page exists
pageID := r.PathValue("PageID")
if pageID == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "PageID" must be provided`)).
ServeHTTP(w, r)
return
}
interactionID := r.PathValue("InteractionID")
switch getMethod(r) {
case http.MethodPost:
router.addInteraction(w, r)
case http.MethodDelete:
if interactionID == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "InteractionID" must be provided`)).
ServeHTTP(w, r)
return
}
router.deleteInteraction(w, r)
default:
exception.
MethodNotAllowed([]string{
http.MethodPost,
http.MethodDelete,
}).
ServeHTTP(w, r)
}
}
func (router *router) addInteraction(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
id := r.PathValue("ID")
router.assert.NotZero(id, "This method should be used after the path values are checked")
pageID := r.PathValue("PageID")
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
// TODO: Methods to manipulate interactions, instead of router need to do this logic
page, err := router.service.GetPage(id, pageID)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
page.Image = nil // HACK: Prevent image update on S3
x, err := strconv.ParseUint(r.FormValue("x"), 10, 0)
if err != nil {
exception.
BadRequest(errors.Join(errors.New(`value "x" should be a valid non-negative integer`), err)).
ServeHTTP(w, r)
return
}
y, err := strconv.ParseUint(r.FormValue("y"), 10, 0)
if err != nil {
exception.
BadRequest(errors.Join(errors.New(`value "y" should be a valid non-negative integer`), err)).
ServeHTTP(w, r)
return
}
link := r.FormValue("link")
if link == "" {
exception.BadRequest(errors.New(`missing parameter "link" in request`)).ServeHTTP(w, r)
return
}
intID, err := randstr.NewHex(6)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
page.Interactions[intID] = service.PageInteraction{
X: uint16(x),
Y: uint16(y),
URL: link,
}
err = router.service.UpdatePage(id, page)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther)
}
func (router *router) deleteInteraction(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
id := r.PathValue("ID")
router.assert.NotZero(id, "This method should be used after the path values are checked")
pageID := r.PathValue("PageID")
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
interactionID := r.PathValue("InteractionID")
router.assert.NotZero(interactionID, "This method should be used after the path values are checked")
// TODO: Methods to manipulate interactions, instead of router need to do this logic
page, err := router.service.GetPage(id, pageID)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
page.Image = nil // HACK: Prevent image update on S3
delete(page.Interactions, interactionID)
err = router.service.UpdatePage(id, page)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther)
}

View File

@@ -1,181 +0,0 @@
package router
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"path"
"forge.capytal.company/capytalcode/project-comicverse/service"
"forge.capytal.company/loreddev/x/smalltrip/exception"
)
func (router *router) projects(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
switch getMethod(r) {
case http.MethodGet, http.MethodHead:
if id := r.PathValue("ID"); id != "" {
router.getProject(w, r)
} else {
router.listProjects(w, r)
}
case http.MethodPost:
router.createProject(w, r)
case http.MethodDelete:
if id := r.PathValue("ID"); id != "" {
router.deleteProject(w, r)
} else {
exception.
BadRequest(errors.New(`missing "ID" path value`)).
ServeHTTP(w, r)
}
default:
exception.MethodNotAllowed([]string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodDelete,
}).ServeHTTP(w, r)
}
}
func (router *router) createProject(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
if getMethod(r) != http.MethodPost {
exception.
MethodNotAllowed([]string{http.MethodPost}).
ServeHTTP(w, r)
return
}
p, err := router.service.CreateProject()
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
router.assert.NotZero(p.ID)
http.Redirect(w, r, fmt.Sprintf("%s/", path.Join(r.URL.Path, p.ID)), http.StatusSeeOther)
}
func (router *router) getProject(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
router.assert.NotNil(router.templates)
if getMethod(r) != http.MethodGet && getMethod(r) != http.MethodHead {
exception.
MethodNotAllowed([]string{http.MethodGet, http.MethodHead}).
ServeHTTP(w, r)
return
}
id := r.PathValue("ID")
if id == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
ServeHTTP(w, r)
return
}
p, err := router.service.GetProject(id)
switch {
case errors.Is(err, service.ErrProjectNotExists):
exception.NotFound().ServeHTTP(w, r)
return
case err != nil:
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
err = router.templates.ExecuteTemplate(w, "project", p)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
}
func (router *router) listProjects(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
router.assert.NotNil(router.templates)
if getMethod(r) != http.MethodGet && getMethod(r) != http.MethodHead {
exception.
MethodNotAllowed([]string{http.MethodGet, http.MethodHead}).
ServeHTTP(w, r)
return
}
ps, err := router.service.ListProjects()
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
b, err := json.Marshal(ps)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write(b)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
}
func (router *router) deleteProject(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(w)
router.assert.NotNil(r)
router.assert.NotNil(router.service)
router.assert.NotNil(router.templates)
if getMethod(r) != http.MethodDelete {
exception.
MethodNotAllowed([]string{http.MethodDelete}).
ServeHTTP(w, r)
return
}
id := r.PathValue("ID")
if id == "" {
exception.
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
ServeHTTP(w, r)
return
}
err := router.service.DeleteProject(id)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
err = router.templates.ExecuteTemplate(w, "partials-status", map[string]any{
"StatusCode": http.StatusOK,
"Message": fmt.Sprintf("Project %q successfully deleted", id),
"Redirect": "/dashboard/",
"RedirectMessage": "Go back to dashboard",
})
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
}

View File

@@ -5,7 +5,6 @@ import (
"io/fs"
"log/slog"
"net/http"
"strings"
"forge.capytal.company/capytalcode/project-comicverse/service"
"forge.capytal.company/capytalcode/project-comicverse/templates"
@@ -95,44 +94,6 @@ func (router *router) setup() http.Handler {
r.HandleFunc("/dashboard/", router.dashboard)
r.HandleFunc("/projects/{$}", router.projects)
r.HandleFunc("/projects/{ID}/", router.projects)
r.HandleFunc("/projects/{ID}/pages/{$}", router.pages)
r.HandleFunc("/projects/{ID}/pages/{PageID}", router.pages)
r.HandleFunc("/projects/{ID}/pages/{PageID}/interactions/{$}", router.interactions)
r.HandleFunc("/projects/{ID}/pages/{PageID}/interactions/{InteractionID}", router.interactions)
return r
}
func (router *router) dashboard(w http.ResponseWriter, r *http.Request) {
router.assert.NotNil(router.templates)
router.assert.NotNil(router.service)
router.assert.NotNil(w)
router.assert.NotNil(r)
p, err := router.service.ListProjects()
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
return
}
w.WriteHeader(http.StatusOK)
err = router.templates.ExecuteTemplate(w, "dashboard", p)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
}
}
func getMethod(r *http.Request) string {
if r.Method == http.MethodGet || r.Method == http.MethodHead {
return r.Method
}
m := r.FormValue("x-method")
if m == "" {
return r.Method
}
return strings.ToUpper(m)
}

View File

@@ -1,180 +0,0 @@
package service
import (
"errors"
"fmt"
"io"
"net/http"
"slices"
"forge.capytal.company/capytalcode/project-comicverse/internals/randstr"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
const pageIDLength = 6
var ErrPageNotExists = errors.New("page does not exists in storage")
type Project struct {
ID string `json:"id"`
Title string `json:"title"`
Pages []ProjectPage `json:"pages"`
}
type ProjectPage struct {
ID string `json:"id"`
Interactions map[string]PageInteraction `json:"interactions"`
Image io.ReadCloser `json:"-"`
}
type PageInteraction struct {
URL string `json:"url"`
X uint16 `json:"x"`
Y uint16 `json:"y"`
}
func (s *Service) AddPage(projectID string, img io.Reader) error {
s.assert.NotNil(s.ctx)
s.assert.NotNil(s.s3)
s.assert.NotNil(s.bucket)
s.assert.NotZero(projectID)
s.assert.NotNil(img)
id, err := randstr.NewHex(pageIDLength)
if err != nil {
return err
}
p, err := s.GetProject(projectID)
if err != nil {
return errors.Join(errors.New("unable to get project"), err)
}
p.Pages = append(p.Pages, ProjectPage{ID: id, Interactions: map[string]PageInteraction{}})
k := fmt.Sprintf("%s/%s", projectID, id)
_, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{
Key: &k,
Body: img,
Bucket: &s.bucket,
})
if err != nil {
return err
}
err = s.UpdateProject(projectID, p)
return err
}
func (s *Service) GetPage(projectID string, pageID string) (ProjectPage, error) {
s.assert.NotNil(s.ctx)
s.assert.NotNil(s.s3)
s.assert.NotNil(s.bucket)
s.assert.NotZero(projectID)
s.assert.NotNil(pageID)
p, err := s.GetProject(projectID)
if err != nil {
return ProjectPage{}, errors.Join(errors.New("unable to get project"), err)
}
pageIndex := slices.IndexFunc(p.Pages, func(p ProjectPage) bool { return p.ID == pageID })
if pageIndex == -1 {
return ProjectPage{}, ErrPageNotExists
}
page := p.Pages[pageIndex]
k := fmt.Sprintf("%s/%s", projectID, pageID)
res, err := s.s3.GetObject(s.ctx, &s3.GetObjectInput{
Key: &k,
Bucket: &s.bucket,
})
if err != nil {
var resErr *awshttp.ResponseError
if errors.As(err, &resErr) && resErr.ResponseError.HTTPStatusCode() == http.StatusNotFound {
// TODO: This would probably be better in some background "maintenance" worker
p.Pages = slices.Delete(p.Pages, pageIndex, pageIndex)
_ = s.UpdateProject(projectID, p)
return ProjectPage{}, errors.Join(ErrPageNotExists, resErr)
}
return ProjectPage{}, err
}
s.assert.NotNil(res.Body)
s.assert.NotNil(page.Interactions)
page.Image = res.Body
return page, nil
}
func (s *Service) UpdatePage(projectID string, page ProjectPage) error {
s.assert.NotNil(s.ctx)
s.assert.NotNil(s.s3)
s.assert.NotNil(s.bucket)
s.assert.NotZero(projectID)
s.assert.NotZero(page.ID)
s.assert.NotNil(page.Interactions)
p, err := s.GetProject(projectID)
if err != nil {
return errors.Join(errors.New("unable to get project"), err)
}
pageIndex := slices.IndexFunc(p.Pages, func(p ProjectPage) bool { return p.ID == page.ID })
if pageIndex == -1 {
return ErrPageNotExists
}
p.Pages[pageIndex] = page
// TODO: Probably a "lastUpdated" timestamp in the ProjectPage data would be better
// so we don't update equal images. Changing the image in ProjectPage would be better
// using a method, or could be completely decoupled from the struct.
if page.Image != nil {
k := fmt.Sprintf("%s/%s", projectID, page.ID)
_, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{
Key: &k,
Body: page.Image,
Bucket: &s.bucket,
})
if err != nil {
return errors.Join(errors.New("error while trying to update image"), err)
}
}
err = s.UpdateProject(projectID, p)
if err != nil {
return errors.Join(errors.New("error while trying to update project"), err)
}
return nil
}
func (s *Service) DeletePage(projectID string, id string) error {
s.assert.NotNil(s.ctx)
s.assert.NotNil(s.s3)
s.assert.NotNil(s.bucket)
s.assert.NotZero(projectID)
s.assert.NotNil(id)
p, err := s.GetProject(projectID)
if err != nil {
return errors.Join(errors.New("unable to get project"), err)
}
k := fmt.Sprintf("%s/%s", projectID, id)
_, err = s.s3.DeleteObject(s.ctx, &s3.DeleteObjectInput{
Key: &k,
Bucket: &s.bucket,
})
if err != nil {
return err
}
p.Pages = slices.DeleteFunc(p.Pages, func(p ProjectPage) bool { return p.ID == id })
err = s.UpdateProject(projectID, p)
return err
}

View File

@@ -1,195 +0,0 @@
package service
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"forge.capytal.company/capytalcode/project-comicverse/internals/randstr"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
const projectIDLength = 6
var ErrProjectNotExists = errors.New("project does not exists in database")
func (s *Service) CreateProject() (Project, error) {
s.assert.NotNil(s.db)
s.assert.NotNil(s.s3)
s.assert.NotNil(s.ctx)
s.assert.NotZero(s.bucket)
s.log.Debug("Creating new project")
id, err := randstr.NewHex(projectIDLength)
if err != nil {
return Project{}, errors.Join(errors.New("creating hexadecimal ID returned error"), err)
}
title := "New Project"
s.assert.NotZero(id, "ID should never be empty")
s.log.Debug("Creating project on database", slog.String("id", id))
_, err = s.db.CreateProject(id, title)
if err != nil {
return Project{}, err
}
p := Project{
ID: id,
Title: title,
Pages: []ProjectPage{},
}
c, err := json.Marshal(p)
if err != nil {
return Project{}, err
}
s.log.Debug("Creating project on storage", slog.String("id", id))
f := fmt.Sprintf("%s.comic.json", id)
_, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{
Bucket: &s.bucket,
Key: &f,
Body: bytes.NewReader(c),
})
if err != nil {
return Project{}, err
}
return p, nil
}
func (s *Service) GetProject(id string) (Project, error) {
s.assert.NotNil(s.db)
s.assert.NotNil(s.s3)
s.assert.NotZero(s.bucket)
s.assert.NotNil(s.ctx)
s.assert.NotZero(id)
res, err := s.db.GetProject(id)
// if errors.Is(err, database.ErrNoRows) {
// return Project{}, errors.Join(ErrProjectNotExists, err)
// }
if err != nil {
return Project{}, err
}
f := fmt.Sprintf("%s.comic.json", id)
file, err := s.s3.GetObject(s.ctx, &s3.GetObjectInput{
Bucket: &s.bucket,
Key: &f,
})
if err != nil {
return Project{}, err
}
c, err := io.ReadAll(file.Body)
if err != nil {
return Project{}, err
}
var p Project
err = json.Unmarshal(c, &p)
s.assert.Equal(res.ID, p.ID, "The project ID should always be equal in the Database and Storage")
s.assert.Equal(res.Title, p.Title)
return p, err
}
func (s *Service) ListProjects() ([]Project, error) {
s.assert.NotNil(s.db)
ps, err := s.db.ListProjects()
if err != nil {
return []Project{}, err
}
p := make([]Project, len(ps))
for i, dp := range ps {
// TODO: this is temporally for debugging, getting every project
// from s3 can be expensive
v, err := s.GetProject(dp.ID)
if err != nil {
return []Project{}, err
}
p[i] = v
}
return p, nil
}
func (s *Service) UpdateProject(id string, project Project) error {
s.assert.NotNil(s.db)
s.assert.NotNil(s.s3)
s.assert.NotZero(s.bucket)
s.assert.NotNil(s.ctx)
s.assert.NotZero(id)
c, err := json.Marshal(project)
if err != nil {
return err
}
s.log.Debug("Updating project on storage", slog.String("id", id))
f := fmt.Sprintf("%s.comic.json", id)
_, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{
Bucket: &s.bucket,
Body: bytes.NewReader(c),
Key: &f,
})
if err != nil {
return err
}
return nil
}
func (s *Service) DeleteProject(id string) error {
s.assert.NotNil(s.db)
s.assert.NotNil(s.s3)
s.assert.NotZero(s.bucket)
s.assert.NotNil(s.ctx)
s.assert.NotZero(id)
p, err := s.GetProject(id)
if err != nil {
return errors.Join(errors.New("unable to get information of project"), err)
}
err = s.db.DeleteProject(id)
if err != nil {
return err
}
s.log.Debug("Deleting project on storage", slog.String("id", id))
files := []types.ObjectIdentifier{}
f := fmt.Sprintf("%s.comic.json", id)
files = append(files, types.ObjectIdentifier{Key: &f})
for k := range p.Pages {
f := fmt.Sprintf("%s/%s", id, k)
files = append(files, types.ObjectIdentifier{Key: &f})
}
_, err = s.s3.DeleteObjects(s.ctx, &s3.DeleteObjectsInput{
Delete: &types.Delete{Objects: files},
Bucket: &s.bucket,
})
if err != nil {
return err
}
return nil
}