diff --git a/_frontend/src/appunti-condivisi/main.jsx b/_frontend/src/appunti-condivisi/main.jsx index e5fcb13..93d6659 100644 --- a/_frontend/src/appunti-condivisi/main.jsx +++ b/_frontend/src/appunti-condivisi/main.jsx @@ -329,7 +329,7 @@ const TabellaApprovazioni = ({ pendingApprovazioni }) => { return (
+ {{ .Dispensa.Description }} +
+{{end}} \ No newline at end of file diff --git a/appunti/appunti.go b/appunti/appunti.go index 4ee6c47..d2de2be 100644 --- a/appunti/appunti.go +++ b/appunti/appunti.go @@ -2,16 +2,51 @@ package appunti import ( "git.phc.dm.unipi.it/phc/website/database" + "git.phc.dm.unipi.it/phc/website/model" ) +// Service isola l'handler dal modulo del database e si occupa solo delle interazioni riguardanti gli appunti e le dispense type Service interface { - ViewDispense() error + GetDispensa(id string) (*model.Dispensa, error) + CreateDispensa(template model.Dispensa) (string, error) + SetDispensaTags(dispensaId string, tags []string) error + // GetDispensaTags(dispensaId string) ([]string, error) } type DefaultService struct { - DB database.DB + DB *database.DB } -func (s *DefaultService) CreateDispensa(user string, template database.Dispensa) { +var _ Service = &DefaultService{} +func (s *DefaultService) GetDispensa(id string) (*model.Dispensa, error) { + dispensa, err := s.DB.GetDispensa(id) + if err != nil { + return nil, err + } + + tags, err := s.DB.GetTags(id) + if err != nil { + return nil, err + } + + return &model.Dispensa{ + Id: dispensa.Id, + OwnerId: dispensa.OwnerId, + Title: dispensa.Title, + Description: dispensa.Description, + Tags: tags, + }, nil +} + +func (s *DefaultService) CreateDispensa(template model.Dispensa) (string, error) { + return s.DB.CreateDispensa(database.Dispensa{ + OwnerId: template.OwnerId, + Title: template.Title, + Description: template.Description, + }) +} + +func (s *DefaultService) SetDispensaTags(dispensaId string, tags []string) error { + return s.DB.SetTags(dispensaId, tags) } diff --git a/cmd/phc-website-server/main.go b/cmd/phc-website-server/main.go index c5843f1..fc64820 100644 --- a/cmd/phc-website-server/main.go +++ b/cmd/phc-website-server/main.go @@ -3,28 +3,32 @@ package main import ( "log" + "git.phc.dm.unipi.it/phc/website/appunti" "git.phc.dm.unipi.it/phc/website/articles" "git.phc.dm.unipi.it/phc/website/auth" "git.phc.dm.unipi.it/phc/website/config" + "git.phc.dm.unipi.it/phc/website/database" "git.phc.dm.unipi.it/phc/website/handler" "git.phc.dm.unipi.it/phc/website/lista_utenti" "git.phc.dm.unipi.it/phc/website/server" "git.phc.dm.unipi.it/phc/website/storia" "git.phc.dm.unipi.it/phc/website/templates" + "git.phc.dm.unipi.it/phc/website/util" ) func main() { config.Load() - authService := auth.NewDefaultService(config.AuthServiceHost) + auth := auth.NewDefaultService(config.AuthServiceHost) - listaUtentiService, err := lista_utenti.New(authService, config.ListaUtenti) - if err != nil { - log.Fatal(err) + // Create database connection and apply pending migrations + db := util.Must(database.NewSqlite3Database("phc-server.local.db")) + if err := db.Migrate("./database/migrations"); err != nil { + panic(err) } - h := &handler.DefaultHandler{ - AuthService: authService, + app := server.NewFiberServer(&handler.DefaultHandler{ + AuthService: auth, Renderer: templates.NewRenderer( "./_views/", "./_views/base.html", @@ -35,13 +39,14 @@ func main() { Storia: &storia.JsonFileStoria{ Path: "./_content/storia.yaml", }, - ListaUtenti: listaUtentiService, - } - - app := server.NewFiberServer(h) + ListaUtenti: util.Must(lista_utenti.New(auth, config.ListaUtenti)), + AppuntiService: &appunti.DefaultService{ + DB: db, + }, + }) log.Printf("Starting server on host %q", config.Host) if err := app.Listen(config.Host); err != nil { - log.Fatal(err) + panic(err) } } diff --git a/database/database.go b/database/database.go index ce076a2..5ebeec9 100644 --- a/database/database.go +++ b/database/database.go @@ -1,15 +1,11 @@ package database -import "time" - // Tools type DBMigrate interface { Migrate(migrationDir string) error } -// Entities - type DBDispense interface { CreateDispensa(template Dispensa) (string, error) GetDispensa(id string) (Dispensa, error) @@ -18,74 +14,34 @@ type DBDispense interface { DeleteDispensa(id string) error } -type DBUpload interface { - CreateUpload(template UploadedContent) (string, error) - DeleteUpload(id UploadedContent) (UploadedContent, error) +type DBUploads interface { + CreateUpload(template Upload) (string, error) + GetUpload(id string) (Upload, error) } -type DBHashApprovals interface { - CreateApprovedHash(a HashApproval) error - GetApprovedHash(hash string) (HashApproval, error) - AllApprovedHash(a HashApproval) error +type DBFileApprovals interface { + CreateFileApproval(template FileApproval) (string, error) + GetFileApproval(id string) (FileApproval, error) + AllFileApprovals() ([]FileApproval, error) + UpdateFileApproval(d FileApproval) error + DeleteFileApproval(id string) error } -type DBHashRejections interface { - CreateRejectedHash(a HashRejection) error - GetRejectedHash(hash string) (HashRejection, error) - AllRejectedHash(a HashRejection) error +type DBDownloads interface { + CreateDownload(template Download) (string, error) } -// Relations - type DBTags interface { - SetTags(dispensaId string, tag []string) error + SetTags(dispensaId string, tags []string) error GetTags(dispensaId string) ([]string, error) } -type DBOwners interface { - CreateOwner(owner, owned string) error - GetOwner(ownedId string) (string, error) -} - -type DBAuthors interface { - CreateAuthor(userId, dispensaId string) error - GetAuthorId(dispensaId string) (string, error) - GetUserDispenseIds(user string) ([]string, error) -} - -type DBCreationTimes interface { - CreateCreationTime(entityId string, createdAt time.Time) error - GetCreationTime(entityId string) (time.Time, error) -} - -type DBOther interface { - // AllDispenseOfUser ritorna tutte le dispense di un certo utente - AllDispenseOfUser(username string) ([]Dispensa, error) - // AllLatestsApprovedDispenseUploads ritorna una lista contenente tutte le dispense con almeno un upload approvato e ritorna la coppia della (dispensa, upload piĆ¹ recente) - AllLatestsApprovedDispenseUploads() ([]struct { - Dispensa - UploadedContent - }, error) - // AllDispenseWithState ... - AllDispenseWithState(username string) ([]struct { - Dispensa - State string - }, error) -} - // DB main "interface group" for interacting with the database, with this technique we can test each "table" api of the DB in isolation. type DB struct { DBMigrate - DBDispense - DBUpload - DBHashApprovals - DBHashRejections - + DBUploads + DBFileApprovals + DBDownloads DBTags - DBOwners - DBAuthors - DBCreationTimes - - DBOther } diff --git a/database/migrations/0001-2022-08-31.sql b/database/migrations/0001-2022-08-31.sql index d075af6..869026a 100644 --- a/database/migrations/0001-2022-08-31.sql +++ b/database/migrations/0001-2022-08-31.sql @@ -1,8 +1,4 @@ --- --- Entities --- - -- Dispense CREATE TABLE IF NOT EXISTS "dispense"( "id" TEXT NOT NULL PRIMARY KEY, @@ -22,7 +18,7 @@ CREATE TABLE IF NOT EXISTS "uploads"( FOREIGN KEY (dispensa_id) REFERENCES dispense(id) ); --- Contenuti con hash approvati +-- Approvazioni contenuti caricati CREATE TABLE IF NOT EXISTS "file_approvals"( "id" TEXT NOT NULL PRIMARY KEY, "created_at" TEXT NOT NULL, @@ -37,11 +33,7 @@ CREATE TABLE IF NOT EXISTS "downloads"( "dispensa_id" TEXT NOT NULL, "timestamp" TEXT NOT NULL, FOREIGN KEY (dispensa_id) REFERENCES dispense(id) -) - --- --- Relations --- +); -- Tags per le dispense CREATE TABLE IF NOT EXISTS "tags"( diff --git a/database/model.go b/database/model.go index ca83b5f..a5557fc 100644 --- a/database/model.go +++ b/database/model.go @@ -2,43 +2,34 @@ package database type Dispensa struct { Id string `db:"id"` + CreatedAt string `db:"created_at"` + OwnerId string `db:"owner_id"` Title string `db:"title"` Description string `db:"description"` } -type Tag struct { +type Upload struct { + Id string `db:"id"` + CreatedAt string `db:"created_at"` + OwnerId string `db:"owner_id"` DispensaId string `db:"dispensa_id"` - Tag string `db:"tag"` -} - -type UploadedContent struct { - Id string `db:"id"` - Hash string `db:"hash"` -} - -type HashApproval struct { - Id string `db:"id"` - Hash string `db:"hash"` -} - -type HashRejection struct { - Id string `db:"id"` - Hash string `db:"hash"` + File string `db:"file"` } -// Relations - -type Owner struct { - OwnerId string `db:"owner_id"` - OwnedId string `db:"owned_id"` +type FileApproval struct { + Id string `db:"id"` + CreatedAt string `db:"created_at"` + OwnerId string `db:"owner_id"` + UploadId string `db:"upload_id"` + Status string `db:"status"` } -type Author struct { - UserId string `db:"user_id"` +type Download struct { DispensaId string `db:"dispensa_id"` + Timestamp string `db:"timestamp"` } -type CreationTime struct { - EntityId string `db:"entity_id"` - CreatedAt string `db:"created_at"` +type Tag struct { + DispensaId string `db:"dispensa_id"` + Tag string `db:"tag"` } diff --git a/database/sqlite.go b/database/sqlite.go index 69de180..cae6a2e 100644 --- a/database/sqlite.go +++ b/database/sqlite.go @@ -22,10 +22,12 @@ type sqliteDB struct { } func (db *sqliteDB) Migrate(migrationFolder string) error { + log.Printf(`Creating migrations table`) if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations(timestamp TEXT, filename TEXT)`); err != nil { return err } + log.Printf(`Loading applied migrations`) appliedMigrations := []migration{} if err := db.Select(&appliedMigrations, `SELECT * FROM migrations`); err != nil { return err @@ -86,13 +88,14 @@ func (db *sqliteDB) Migrate(migrationFolder string) error { } func (db *sqliteDB) CreateDispensa(template Dispensa) (string, error) { - template.Id = "dispensa/" + util.GenerateRandomString(15) + template.Id = "dispensa/" + util.GenerateRandomString(8) + template.CreatedAt = time.Now().Format(time.RFC3339) if _, err := db.NamedExec(` INSERT INTO - dispense(id, title, description) + dispense(id, created_at, owner_id, title, description) VALUES - (:id, :title, :description) + (:id, :created_at, :owner_id, :title, :description) `, &template); err != nil { return "", err } @@ -102,7 +105,6 @@ func (db *sqliteDB) CreateDispensa(template Dispensa) (string, error) { func (db *sqliteDB) GetDispensa(id string) (Dispensa, error) { var dispensa Dispensa - if err := db.Get(&dispensa, `SELECT * FROM dispense WHERE id = ?`, id); err != nil { return Dispensa{}, err } @@ -112,7 +114,6 @@ func (db *sqliteDB) GetDispensa(id string) (Dispensa, error) { func (db *sqliteDB) AllDispense() ([]Dispensa, error) { var dispense []Dispensa - if err := db.Select(&dispense, `SELECT * FROM dispense`); err != nil { return nil, err } @@ -122,10 +123,14 @@ func (db *sqliteDB) AllDispense() ([]Dispensa, error) { func (db *sqliteDB) UpdateDispensa(d Dispensa) error { if _, err := db.NamedExec(` - UPDATE dispense - SET title = :title, - description = :description - WHERE id = :id + UPDATE + dispense + SET + owner_id = :owner_id + title = :title, + description = :description + WHERE + id = :id `, &d); err != nil { return err } @@ -137,8 +142,32 @@ func (db *sqliteDB) DeleteDispensa(id string) error { panic("TODO: Not implemented") } +func (db *sqliteDB) CreateUpload(template Upload) (string, error) { + template.Id = "upload/" + util.GenerateRandomString(8) + + if _, err := db.NamedExec(` + INSERT INTO + uploads(id, created_at, owner_id, dispensa_id, file) + VALUES + (:id, :created_at, :owner_id, :dispensa_id, :file) + `, &template); err != nil { + return "", err + } + + return template.Id, nil +} + +func (db *sqliteDB) GetUpload(id string) (Upload, error) { + var upload Upload + if err := db.Select(`SELECT * FROM uploads WHERE id = ?`, id); err != nil { + return Upload{}, err + } + + return upload, nil +} + func NewSqlite3Database(filename string) (*DB, error) { - sqlDB, err := sqlx.Open("sqlite3", filename) + sqlDB, err := sqlx.Open("sqlite3", filename+"?_fk=1") if err != nil { panic(err) } @@ -153,9 +182,6 @@ func NewSqlite3Database(filename string) (*DB, error) { return &DB{ DBMigrate: db, DBDispense: db, - // DBUpload: db, - // DBApprovedHashes: db, - // DBRejectedHashes: db, - // DBOther: db, + DBUploads: db, }, nil } diff --git a/handler/handler.go b/handler/handler.go index be8109d..5c307ad 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -5,6 +5,7 @@ import ( "html/template" "io" + "git.phc.dm.unipi.it/phc/website/appunti" "git.phc.dm.unipi.it/phc/website/articles" "git.phc.dm.unipi.it/phc/website/auth" "git.phc.dm.unipi.it/phc/website/lista_utenti" @@ -16,22 +17,51 @@ import ( ) type Service interface { + // + // Pages + // HandleStaticPage(w io.Writer, view string, ctx Context) error - HandleUtenti() ([]*model.User, error) - HandleListaUtenti() ([]*model.ListUser, error) + + // Storia HandleStoriaPage(w io.Writer, ctx Context) error - HandleQueryAppunti(w io.Writer, query string, ctx Context) error + + // Appunti + HandleAppuntiPage(w io.Writer, query string, ctx Context) error HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error + HandleDispensaPage(w io.Writer, id string, ctx Context) error + + // News HandleNewsPage(w io.Writer, ctx Context) error + + // Guide HandleGuidePage(w io.Writer, ctx Context) error - HandleLogin(username, password string) (*model.Session, error) - HandleUser(token string) *model.User - HandleRequiredUser(ctx Context) (*model.User, error) + + // User HandleProfilePage(w io.Writer, ctx Context) error + + // Article Pages HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error + + // RSS HandleNewsFeedPage(w io.Writer) error HandleGuideFeedPage(w io.Writer) error + + // + // API + // + + // User + HandleLogin(username, password string) (*model.Session, error) + HandleUser(token string) *model.User + HandleRequiredUser(ctx Context) (*model.User, error) + + // User Listing + HandleUtenti() ([]*model.User, error) + HandleListaUtenti() ([]*model.ListUser, error) + + // Appunti + HandleCreateDispensa(template model.Dispensa, ctx Context) (*model.Dispensa, error) } // @@ -48,6 +78,7 @@ func (ctx Context) getUser() *model.User { // Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service) type DefaultHandler struct { AuthService auth.Service + AppuntiService appunti.Service Renderer *templates.TemplateRenderer NewsArticlesRegistry *articles.Registry GuideArticlesRegistry *articles.Registry @@ -55,6 +86,8 @@ type DefaultHandler struct { Storia storia.StoriaService } +var _ Service = &DefaultHandler{} + func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error { return h.Renderer.Render(w, view, util.Map{ "User": ctx.getUser(), @@ -91,7 +124,7 @@ func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error { }) } -func (h *DefaultHandler) HandleQueryAppunti(w io.Writer, query string, ctx Context) error { +func (h *DefaultHandler) HandleAppuntiPage(w io.Writer, query string, ctx Context) error { return h.Renderer.Render(w, "appunti.html", util.Map{ "User": ctx.getUser(), "Query": query, @@ -104,6 +137,18 @@ func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) er }) } +func (h *DefaultHandler) HandleDispensaPage(w io.Writer, id string, ctx Context) error { + dispensa, err := h.AppuntiService.GetDispensa(id) + if err != nil { + return err + } + + return h.Renderer.Render(w, "dispensa.html", util.Map{ + "User": ctx.getUser(), + "Dispensa": dispensa, + }) +} + func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error { articles, err := h.NewsArticlesRegistry.GetArticles() if err != nil { @@ -221,3 +266,28 @@ func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error { return guideFeed.WriteRss(w) } + +// +// API +// + +func (h *DefaultHandler) HandleCreateDispensa(template model.Dispensa, ctx Context) (*model.Dispensa, error) { + user := ctx.getUser() + if user == nil { + return nil, ErrNoUser + } + + template.OwnerId = user.Username + id, err := h.AppuntiService.CreateDispensa(template) + if err != nil { + return nil, err + } + + template.Id = id + + if len(template.Tags) > 0 { + h.AppuntiService.SetDispensaTags(id, template.Tags) + } + + return &template, nil +} diff --git a/model/appunti.go b/model/appunti.go index b8166ad..d74f0ff 100644 --- a/model/appunti.go +++ b/model/appunti.go @@ -1,44 +1,9 @@ package model -import "time" - -// Tables - type Dispensa struct { - // Id is a unique identifier of this object - Id string - // CreatedAt is the time of creation of this object - CreatedAt time.Time - // Title of this dispensa - Title string - // Description of this dispensa - Description string - // Tags for this dispensa, used by search to easily categorize dispense - Tags string -} - -// Upload represents a file identified by its hash and stored in the appunti content store -type Upload struct { - // Id is a unique identifier of this object - Id string - // CreatedAt is the time of creation of this object - CreatedAt time.Time - // Hash of this file - Hash string -} - -// ApprovedHash represents an approved hash -type ApprovedHash struct { - // Hash being approved - Hash string - // CreatedAt is the creation time of this object - CreatedAt time.Time -} - -// RejectedHash represents a rejected hash -type RejectedHash struct { - // Hash being rejected - Hash string - // CreatedAt is the creation time of this object - CreatedAt time.Time + Id string `json:"id"` + OwnerId string `json:"ownerId"` + Title string `json:"title"` + Description string `json:"description"` + Tags []string `json:"tags"` } diff --git a/server/fiber.go b/server/fiber.go index 9526920..55f1d58 100644 --- a/server/fiber.go +++ b/server/fiber.go @@ -115,7 +115,7 @@ func routes(h handler.Service, r fiber.Router) { query := c.Query("q", "") c.Type("html") - return h.HandleQueryAppunti(c, query, CreateContext(c)) + return h.HandleAppuntiPage(c, query, CreateContext(c)) }) r.Get("/appunti/condivisi", func(c *fiber.Ctx) error { @@ -222,4 +222,18 @@ func routesApi(h handler.Service, r fiber.Router) { return c.JSON(user) }) + + r.Post("/appunti", func(c *fiber.Ctx) error { + var dispensaTemplate model.Dispensa + if err := c.BodyParser(&dispensaTemplate); err != nil { + return err + } + + dispensa, err := h.HandleCreateDispensa(dispensaTemplate, CreateContext(c)) + if err != nil { + return err + } + + return c.JSON(dispensa) + }) } diff --git a/util/errors.go b/util/errors.go new file mode 100644 index 0000000..00bc16d --- /dev/null +++ b/util/errors.go @@ -0,0 +1,11 @@ +package util + +import "log" + +func Must[T any](value T, err error) T { + if err != nil { + log.Fatal(err) + } + + return value +} diff --git a/util/rand.go b/util/rand.go index f2a8fbe..8779a06 100644 --- a/util/rand.go +++ b/util/rand.go @@ -2,11 +2,11 @@ package util import ( "crypto/rand" - "encoding/base64" + "encoding/hex" "log" ) -// GenerateRandomString generates a random base64 string from a random array of "n" bytes (to prevent padding let n be a multiple of 3) +// GenerateRandomString generates a random hex string from a random array of "n" bytes func GenerateRandomString(n int) string { b := make([]byte, n) @@ -15,5 +15,5 @@ func GenerateRandomString(n int) string { log.Fatal(err) } - return base64.URLEncoding.EncodeToString(b) + return hex.EncodeToString(b) }