From 17ef2d8fd61825dbacae2c896238c4d582c6692f Mon Sep 17 00:00:00 2001 From: Lucas BEE Date: Fri, 21 Jun 2019 10:44:42 +0000 Subject: [PATCH] Add polochons model and handlers --- backend/backend/polochons.go | 120 +++++++++ backend/config/polochon.go | 7 +- backend/polochons/handlers.go | 231 ++++++++++++++++++ backend/routes.go | 9 + backend/users/handlers.go | 37 ++- backend/users/users.go | 46 +++- migrations/0001_initial.down.sql | 1 + .../0004_change_media_category.down.sql | 4 +- migrations/0006_user_token.down.sql | 7 + migrations/0008_polochons.down.sql | 4 + migrations/0008_polochons.up.sql | 64 +++++ 11 files changed, 501 insertions(+), 29 deletions(-) create mode 100644 backend/backend/polochons.go create mode 100644 backend/polochons/handlers.go create mode 100644 migrations/0008_polochons.down.sql create mode 100644 migrations/0008_polochons.up.sql diff --git a/backend/backend/polochons.go b/backend/backend/polochons.go new file mode 100644 index 0000000..a66f3fa --- /dev/null +++ b/backend/backend/polochons.go @@ -0,0 +1,120 @@ +package backend + +import ( + "database/sql" + "fmt" + + "git.quimbo.fr/odwrtw/canape/backend/sqly" + "github.com/jmoiron/sqlx" +) + +const ( + addPolochonQuery = `INSERT INTO + polochons (name, url, token, admin_id) + VALUES ($1, $2, $3, $4) + RETURNING id;` + getPolochonQuery = `SELECT * FROM polochons WHERE name=$1;` + getPolochonByIDQuery = `SELECT * FROM polochons WHERE id=$1;` + getPolochonsByUserIDQuery = `SELECT * FROM polochons WHERE admin_id=$1;` + updatePolochonQuery = `UPDATE polochons SET + name=:name, url=:url, token=:token, auth_token=:auth_token, + admin_id=:admin_id + WHERE id=:id + RETURNING *;` + deletePolochonQuery = `DELETE FROM polochons WHERE id=:id;` + + getAllPolochonsQuery = `SELECT * FROM polochons order by created_at;` +) + +// ErrUnknownPolochon returned when a polochon does'nt exist +var ErrUnknownPolochon = fmt.Errorf("polochons: polochon does'nt exist") + +// Polochon represents a polochon +type Polochon struct { + sqly.BaseModel + Name string `db:"name" json:"name"` + URL string `db:"url" json:"url"` + Token string `db:"token" json:"token"` + AuthToken string `db:"auth_token" json:"auth_token"` + AdminID string `db:"admin_id" json:"admin_id"` + Admin *User `json:"admin"` + Users []*User `json:"users"` +} + +// Get returns polochon with specified name +func GetPolochon(q sqlx.Queryer, name string) (*Polochon, error) { + p := &Polochon{} + err := q.QueryRowx(getPolochonQuery, name).StructScan(p) + if err != nil { + if err == sql.ErrNoRows { + return nil, ErrUnknownPolochon + } + return nil, err + } + return p, nil +} + +// GetByID returns polochon using its id +func GetPolochonByID(q sqlx.Queryer, id string) (*Polochon, error) { + p := &Polochon{} + err := q.QueryRowx(getPolochonByIDQuery, id).StructScan(p) + if err != nil { + if err == sql.ErrNoRows { + return nil, ErrUnknownPolochon + } + return nil, err + } + return p, nil +} + +// GetAll returns all the polochons +func GetAllPolochons(db *sqlx.DB) ([]*Polochon, error) { + polochons := []*Polochon{} + err := db.Select(&polochons, getAllPolochonsQuery) + if err != nil { + return nil, err + } + return polochons, nil +} + +// GetAllByUser returns all the polochons owned by the user +func GetAllPolochonsByUser(db *sqlx.DB, id string) ([]*Polochon, error) { + polochons := []*Polochon{} + err := db.Select(&polochons, getPolochonsByUserIDQuery, id) + if err != nil { + return nil, err + } + return polochons, nil +} + +// Add polochon to database or raises an error +func (p *Polochon) Add(q sqlx.Queryer) error { + var id string + err := q.QueryRowx(addPolochonQuery, p.Name, p.URL, p.Token, p.AdminID).Scan(&id) + if err != nil { + return err + } + p.ID = id + return nil +} + +// Update polochon on database or raise an error +func (p *Polochon) Update(ex *sqlx.DB) error { + rows, err := ex.NamedQuery(updatePolochonQuery, p) + if err != nil { + return err + } + for rows.Next() { + rows.StructScan(p) + } + return nil +} + +// Delete polochon from database or raise an error +func (p *Polochon) Delete(ex *sqlx.DB) error { + _, err := ex.NamedExec(deletePolochonQuery, p) + if err != nil { + return err + } + return nil +} diff --git a/backend/config/polochon.go b/backend/config/polochon.go index cbc1cbf..25e971e 100644 --- a/backend/config/polochon.go +++ b/backend/config/polochon.go @@ -2,6 +2,9 @@ package config // UserPolochon is polochon access parameter for a user type UserPolochon struct { - URL string `json:"url"` - Token string `json:"token"` + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Token string `json:"token"` + Activated bool `json:"activated"` } diff --git a/backend/polochons/handlers.go b/backend/polochons/handlers.go new file mode 100644 index 0000000..9a93b79 --- /dev/null +++ b/backend/polochons/handlers.go @@ -0,0 +1,231 @@ +package polochons + +import ( + "encoding/json" + "fmt" + "net/http" + + "git.quimbo.fr/odwrtw/canape/backend/auth" + "git.quimbo.fr/odwrtw/canape/backend/users" + "git.quimbo.fr/odwrtw/canape/backend/web" + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" +) + +// GetPublicPolochonsHandler returns the public list of polochons +func GetPublicPolochonsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "polochons.GetPublicPolochonsHandler", + }) + + log.Debug("Getting public polochons") + + polochons, err := backend.GetAllPolochons(env.Database) + if err != nil { + return env.RenderError(w, err) + } + + type MinimalPolochon struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } + + mPolochons := []*MinimalPolochon{} + for _, p := range polochons { + mPolochons = append(mPolochons, &MinimalPolochon{ + ID: p.ID, + Name: p.Name, + URL: p.URL, + }) + } + + return env.RenderJSON(w, mPolochons) +} + +// GetPolochonsHandler returns the list of polochons and their users +func GetPolochonsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "polochons.GetPolochonsHandler", + }) + + v := auth.GetCurrentUser(r, env.Log) + user, ok := v.(*users.User) + if !ok { + return env.RenderError(w, fmt.Errorf("invalid user type")) + } + + log.Debug("Getting polochons") + + polochons, err := backend.GetAllPolochonsByUser(env.Database, user.ID) + if err != nil { + return env.RenderError(w, err) + } + + for _, p := range polochons { + users, err := users.GetPolochonUsers(env.Database, p.ID) + if err != nil { + return env.RenderError(w, err) + } + p.Users = users + } + + return env.RenderJSON(w, polochons) +} + +// NewPolochonHandler handles the creation of a new polochon +func NewPolochonHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "polochons.NewPolochonHandler", + }) + + v := auth.GetCurrentUser(r, env.Log) + user, ok := v.(*users.User) + if !ok { + return env.RenderError(w, fmt.Errorf("invalid user type")) + } + + var data struct { + Name string `json:"name"` + URL string `json:"url"` + Token string `json:"token"` + } + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + return env.RenderError(w, err) + } + + for _, c := range []string{ + data.Name, + data.URL, + data.Token, + } { + if c == "" { + return env.RenderError(w, fmt.Errorf("name, url and token are mandatory fields")) + } + } + log.Debugf("creating new polochon ...") + + p := backend.Polochon{ + Name: data.Name, + URL: data.URL, + Token: data.Token, + AdminID: user.ID, + } + + if err := p.Add(env.Database); err != nil { + return env.RenderError(w, err) + } + log.Debugf("new polochon %s created ...", data.Name) + + return env.RenderOK(w, "Polochon created") +} + +// EditPolochonHandler handles the edit of a polochon +func EditPolochonHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "polochons.EditPolochonHandler", + }) + log.Debugf("editing polochon ...") + + v := auth.GetCurrentUser(r, env.Log) + user, ok := v.(*users.User) + if !ok { + return env.RenderError(w, fmt.Errorf("invalid user type")) + } + + // Get the polochon + vars := mux.Vars(r) + id := vars["id"] + p, err := backend.GetPolochonByID(env.Database, id) + if err != nil { + return env.RenderError(w, err) + } + + // Check that the logged-in user is the polochon admin + if p.AdminID != user.ID { + return env.RenderError(w, fmt.Errorf("forbidden")) + } + + var data struct { + Name string `json:"name"` + URL string `json:"url"` + Token string `json:"token"` + } + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + return env.RenderError(w, err) + } + + for _, c := range []string{ + data.Name, + data.URL, + data.Token, + } { + if c == "" { + return env.RenderError(w, fmt.Errorf("name, url and token are mandatory fields")) + } + } + p.Name = data.Name + p.URL = data.URL + p.Token = data.Token + + err = p.Update(env.Database) + if err != nil { + return env.RenderError(w, err) + } + + return env.RenderOK(w, "Polochon updated") +} + +// PolochonDeactivateUserHandler handles the users of a polochon +func PolochonDeactivateUserHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + if err := PolochonActivateUser(env, w, r, false); err != nil { + return env.RenderError(w, err) + } + return env.RenderOK(w, "User deactivated") +} + +// PolochonActivateUserHandler handles the users of a polochon +func PolochonActivateUserHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + if err := PolochonActivateUser(env, w, r, true); err != nil { + return env.RenderError(w, err) + } + return env.RenderOK(w, "User activated") +} + +// PolochonActivateUser activates or deactivate a user's polochon +func PolochonActivateUser(env *web.Env, w http.ResponseWriter, r *http.Request, activated bool) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "polochons.PolochonUserHandler", + }) + log.Debugf("editing polochon users ...") + + v := auth.GetCurrentUser(r, env.Log) + user, ok := v.(*users.User) + if !ok { + return env.RenderError(w, fmt.Errorf("invalid user type")) + } + + // Get the polochon + vars := mux.Vars(r) + id := vars["id"] + p, err := backend.GetPolochonByID(env.Database, id) + if err != nil { + return env.RenderError(w, err) + } + + // Check that the logged-in user is the polochon admin + if p.AdminID != user.ID { + return env.RenderError(w, fmt.Errorf("forbidden")) + } + + // Get the user + userID := vars["user_id"] + u, err := users.GetByID(env.Database, userID) + if err != nil { + return env.RenderError(w, err) + } + + u.PolochonActivated = activated + + return u.Update(env.Database) +} diff --git a/backend/routes.go b/backend/routes.go index 1560739..ad086cc 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -5,6 +5,7 @@ import ( "git.quimbo.fr/odwrtw/canape/backend/events" extmedias "git.quimbo.fr/odwrtw/canape/backend/external_medias" "git.quimbo.fr/odwrtw/canape/backend/movies" + "git.quimbo.fr/odwrtw/canape/backend/polochons" "git.quimbo.fr/odwrtw/canape/backend/ratings" "git.quimbo.fr/odwrtw/canape/backend/shows" "git.quimbo.fr/odwrtw/canape/backend/torrents" @@ -22,6 +23,14 @@ func setupRoutes(env *web.Env) { env.Handle("/users/tokens/{token}", users.EditTokenHandler).WithRole(users.UserRole).Methods("POST") env.Handle("/users/tokens/{token}", users.DeleteTokenHandler).WithRole(users.UserRole).Methods("DELETE") env.Handle("/users/modules/status", users.GetModulesStatus).WithRole(users.UserRole).Methods("GET") + env.Handle("/users/polochons", polochons.GetPolochonsHandler).WithRole(users.UserRole).Methods("GET") + + // Polochon's route + env.Handle("/polochons", polochons.GetPublicPolochonsHandler).Methods("GET") + env.Handle("/polochons", polochons.NewPolochonHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/polochons/{id}", polochons.EditPolochonHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/polochons/{id}/users/{user_id}", polochons.PolochonActivateUserHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/polochons/{id}/users/{user_id}", polochons.PolochonDeactivateUserHandler).WithRole(users.UserRole).Methods("DELETE") // Movies routes env.Handle("/movies/polochon", movies.PolochonMoviesHandler).WithRole(users.UserRole).Methods("GET") diff --git a/backend/users/handlers.go b/backend/users/handlers.go index f0184e6..4671e43 100644 --- a/backend/users/handlers.go +++ b/backend/users/handlers.go @@ -1,6 +1,7 @@ package users import ( + "database/sql" "encoding/json" "fmt" "net/http" @@ -93,10 +94,17 @@ func DetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { } var polochonConfig config.UserPolochon - err := user.GetConfig("polochon", &polochonConfig) - if err != nil { - return err + if user.PolochonID.Valid && user.PolochonID.String != "" { + polochon, err := models.GetPolochonByID(e.Database, user.PolochonID.String) + if err != nil { + return e.RenderError(w, fmt.Errorf("Could not find such polochon")) + } + polochonConfig.Name = polochon.Name + polochonConfig.URL = polochon.URL } + polochonConfig.Token = user.Token + polochonConfig.Activated = user.PolochonActivated + polochonConfig.ID = user.PolochonID.String return e.RenderJSON(w, polochonConfig) } @@ -110,7 +118,7 @@ func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { } var data struct { - PolochonURL string `json:"polochon_url"` + PolochonID string `json:"polochon_id"` PolochonToken string `json:"polochon_token"` Password string `json:"password"` PasswordConfirm string `json:"password_confirm"` @@ -132,17 +140,20 @@ func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { } } - // Update the polochon config - var polochonConfig config.UserPolochon - if err := user.GetConfig("polochon", &polochonConfig); err != nil { - return err - } - polochonConfig.URL = data.PolochonURL - polochonConfig.Token = data.PolochonToken - if err := user.SetConfig("polochon", polochonConfig); err != nil { - return err + if data.PolochonID != "" && user.PolochonID.String != data.PolochonID { + _, err := models.GetPolochonByID(e.Database, data.PolochonID) + if err != nil { + return e.RenderError(w, fmt.Errorf("Could not find such polochon")) + } + user.PolochonID = sql.NullString{ + String: data.PolochonID, + Valid: true, + } + user.PolochonActivated = false } + user.Token = data.PolochonToken + // Save the user with the new configurations if err := user.Update(e.Database); err != nil { return err diff --git a/backend/users/users.go b/backend/users/users.go index 1fe7e8a..c077c33 100644 --- a/backend/users/users.go +++ b/backend/users/users.go @@ -15,13 +15,22 @@ import ( ) const ( - addUserQuery = `INSERT INTO users (name, hash, admin, rawconfig) VALUES ($1, $2, $3, $4) RETURNING id;` + addUserQuery = `INSERT INTO + users (name, hash, admin, polochon_id, token) + VALUES ($1, $2, $3, $4, $5) + RETURNING id;` getUserQuery = `SELECT * FROM users WHERE name=$1;` getUserByIDQuery = `SELECT * FROM users WHERE id=$1;` - updateUserQuery = `UPDATE users SET name=:name, hash=:hash, admin=:admin, activated=:activated, rawconfig=:rawconfig WHERE id=:id RETURNING *;` - deleteUseQuery = `DELETE FROM users WHERE id=:id;` + updateUserQuery = `UPDATE users SET + name=:name, hash=:hash, admin=:admin, activated=:activated, + rawconfig=:rawconfig, polochon_id=:polochon_id, token=:token, + polochon_activated=:polochon_activated + WHERE id=:id + RETURNING *;` + deleteUserQuery = `DELETE FROM users WHERE id=:id;` - getAllUsersQuery = `SELECT * FROM users order by created_at;` + getAllUsersQuery = `SELECT * FROM users order by created_at;` + getPolochonUsersQuery = `SELECT * FROM users WHERE polochon_id = $1;` ) const ( @@ -31,17 +40,20 @@ const ( AdminRole = "admin" ) -// ErrUnknownUser returned web a user does'nt exist +// ErrUnknownUser returned when a user does'nt exist var ErrUnknownUser = fmt.Errorf("users: user does'nt exist") // User represents an user type User struct { sqly.BaseModel - Name string - Hash string - Admin bool - Activated bool - RawConfig types.JSONText + Name string `json:"name"` + Hash string `json:"-"` + Admin bool `json:"admin"` + RawConfig types.JSONText `json:"raw_config"` + Token string `json:"token"` + Activated bool `json:"activated"` + PolochonID sql.NullString `json:"polochon_id" db:"polochon_id"` + PolochonActivated bool `json:"polochon_activated" db:"polochon_activated"` } // GetConfig unmarshal json from specified config key into v @@ -149,10 +161,20 @@ func GetAll(db *sqlx.DB) ([]*User, error) { return users, nil } +// GetPolochonUsers returns all the users of a polochon +func GetPolochonUsers(db *sqlx.DB, id string) ([]*User, error) { + users := []*User{} + err := db.Select(&users, getPolochonUsersQuery, id) + if err != nil { + return nil, err + } + return users, nil +} + // Add user to database or raises an error func (u *User) Add(q sqlx.Queryer) error { var id string - err := q.QueryRowx(addUserQuery, u.Name, u.Hash, u.Admin, u.RawConfig).Scan(&id) + err := q.QueryRowx(addUserQuery, u.Name, u.Hash, u.Admin, u.PolochonID, u.Token).Scan(&id) if err != nil { return err } @@ -174,7 +196,7 @@ func (u *User) Update(ex *sqlx.DB) error { // Delete user from database or raise an error func (u *User) Delete(ex *sqlx.DB) error { - _, err := ex.NamedExec(deleteUseQuery, u) + _, err := ex.NamedExec(deleteUserQuery, u) if err != nil { return err } diff --git a/migrations/0001_initial.down.sql b/migrations/0001_initial.down.sql index ad68a42..78dfeb4 100644 --- a/migrations/0001_initial.down.sql +++ b/migrations/0001_initial.down.sql @@ -1,3 +1,4 @@ +DROP TABLE movies_tracked; DROP TABLE movies; DROP TABLE shows_tracked; DROP TABLE episodes; diff --git a/migrations/0004_change_media_category.down.sql b/migrations/0004_change_media_category.down.sql index 7055da1..0de7ad6 100644 --- a/migrations/0004_change_media_category.down.sql +++ b/migrations/0004_change_media_category.down.sql @@ -1,3 +1,3 @@ ALTER TABLE external_medias ALTER COLUMN category TYPE media_category USING category::media_category; -ALTER TABLE external_medias ALTER COLUMN source TYPE media_category USING source::media_source; -ALTER TABLE external_medias ALTER COLUMN type TYPE media_category USING type::media_type; +ALTER TABLE external_medias ALTER COLUMN source TYPE media_source USING source::media_source; +ALTER TABLE external_medias ALTER COLUMN type TYPE media_type USING type::media_type; diff --git a/migrations/0006_user_token.down.sql b/migrations/0006_user_token.down.sql index 7da0f43..ae1d374 100644 --- a/migrations/0006_user_token.down.sql +++ b/migrations/0006_user_token.down.sql @@ -1 +1,8 @@ DROP TABLE tokens; +CREATE TABLE tokens ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + value text NOT NULL UNIQUE, + user_id uuid REFERENCES users (id) ON DELETE CASCADE, + LIKE base INCLUDING DEFAULTS +); +CREATE TRIGGER update_tokens_updated_at BEFORE UPDATE ON tokens FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); diff --git a/migrations/0008_polochons.down.sql b/migrations/0008_polochons.down.sql new file mode 100644 index 0000000..1c1f16e --- /dev/null +++ b/migrations/0008_polochons.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE users DROP COLUMN polochon_id; +ALTER TABLE users DROP COLUMN token; +ALTER TABLE users DROP COLUMN polochon_activated; +DROP TABLE IF EXISTS polochons; diff --git a/migrations/0008_polochons.up.sql b/migrations/0008_polochons.up.sql new file mode 100644 index 0000000..3588ebe --- /dev/null +++ b/migrations/0008_polochons.up.sql @@ -0,0 +1,64 @@ +CREATE TABLE polochons ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + url text NOT NULL, + token text NOT NULL, + auth_token text NOT NULL DEFAULT gen_random_uuid(), + admin_id uuid NOT NULL REFERENCES users (id), + LIKE base INCLUDING DEFAULTS +); + +CREATE UNIQUE INDEX ON polochons (id); +CREATE INDEX ON polochons (name); +CREATE TRIGGER update_polochons_updated_at BEFORE UPDATE ON polochons FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); + +ALTER TABLE users ADD COLUMN polochon_id uuid DEFAULT NULL REFERENCES polochons (id); +ALTER TABLE users ADD COLUMN token text NOT NULL DEFAULT ''; +ALTER TABLE users ADD COLUMN polochon_activated boolean NOT NULL DEFAULT false; + +CREATE INDEX ON users (polochon_id); + +CREATE OR REPLACE FUNCTION migrate_polochons() RETURNS void AS $$ +DECLARE + u RECORD; + polochon RECORD; + admin RECORD; +BEGIN + -- Retrieve the admin + SELECT * INTO admin FROM users WHERE name = 'admin'; + + FOR u IN select * from users LOOP + -- Check if polochon is configured and the user is activated + IF u.activated = false OR u.rawconfig->'polochon' IS NULL THEN + RAISE NOTICE ' => skipping user %', quote_ident(u.name); + CONTINUE; + END IF; + + RAISE NOTICE 'SELECT * INTO polochon FROM polochons WHERRE name = %', u.rawconfig->'polochon'->>'url'; + -- Try to get the user's polochon + SELECT * INTO polochon FROM polochons WHERE name = u.rawconfig->'polochon'->>'url'; + -- If polochon is null, create it + IF polochon IS NULL THEN + RAISE NOTICE ' => user % have polochon % not yet created, create it!', quote_ident(u.name), u.rawconfig->'polochon'->>'url'; + INSERT INTO polochons (name, url, token, admin_id) + VALUES (u.rawconfig->'polochon'->>'url', u.rawconfig->'polochon'->>'url', u.rawconfig->'polochon'->>'token', admin.id) + RETURNING * INTO polochon; + END IF; + + RAISE NOTICE ' => user % have polochon %', quote_ident(u.name), u.rawconfig->'polochon'->>'url'; + -- Update the user with the token and polochon_id + UPDATE + users + SET + token = u.rawconfig->'polochon'->>'token', + polochon_id = polochon.id, + polochon_activated = true + WHERE + id = u.id; + END LOOP; + RAISE NOTICE 'ALL DONE'; +END; +$$ LANGUAGE plpgsql; + +SELECT * FROM migrate_polochons(); +DROP FUNCTION migrate_polochons();