Add polochons model and handlers

This commit is contained in:
Lucas BEE 2019-06-21 10:44:42 +00:00
parent fda1bc076a
commit 17ef2d8fd6
11 changed files with 501 additions and 29 deletions

View File

@ -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
}

View File

@ -2,6 +2,9 @@ package config
// UserPolochon is polochon access parameter for a user // UserPolochon is polochon access parameter for a user
type UserPolochon struct { type UserPolochon struct {
URL string `json:"url"` ID string `json:"id"`
Token string `json:"token"` Name string `json:"name"`
URL string `json:"url"`
Token string `json:"token"`
Activated bool `json:"activated"`
} }

View File

@ -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)
}

View File

@ -5,6 +5,7 @@ import (
"git.quimbo.fr/odwrtw/canape/backend/events" "git.quimbo.fr/odwrtw/canape/backend/events"
extmedias "git.quimbo.fr/odwrtw/canape/backend/external_medias" extmedias "git.quimbo.fr/odwrtw/canape/backend/external_medias"
"git.quimbo.fr/odwrtw/canape/backend/movies" "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/ratings"
"git.quimbo.fr/odwrtw/canape/backend/shows" "git.quimbo.fr/odwrtw/canape/backend/shows"
"git.quimbo.fr/odwrtw/canape/backend/torrents" "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.EditTokenHandler).WithRole(users.UserRole).Methods("POST")
env.Handle("/users/tokens/{token}", users.DeleteTokenHandler).WithRole(users.UserRole).Methods("DELETE") 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/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 // Movies routes
env.Handle("/movies/polochon", movies.PolochonMoviesHandler).WithRole(users.UserRole).Methods("GET") env.Handle("/movies/polochon", movies.PolochonMoviesHandler).WithRole(users.UserRole).Methods("GET")

View File

@ -1,6 +1,7 @@
package users package users
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@ -93,10 +94,17 @@ func DetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
} }
var polochonConfig config.UserPolochon var polochonConfig config.UserPolochon
err := user.GetConfig("polochon", &polochonConfig) if user.PolochonID.Valid && user.PolochonID.String != "" {
if err != nil { polochon, err := models.GetPolochonByID(e.Database, user.PolochonID.String)
return err 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) return e.RenderJSON(w, polochonConfig)
} }
@ -110,7 +118,7 @@ func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
} }
var data struct { var data struct {
PolochonURL string `json:"polochon_url"` PolochonID string `json:"polochon_id"`
PolochonToken string `json:"polochon_token"` PolochonToken string `json:"polochon_token"`
Password string `json:"password"` Password string `json:"password"`
PasswordConfirm string `json:"password_confirm"` 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 if data.PolochonID != "" && user.PolochonID.String != data.PolochonID {
var polochonConfig config.UserPolochon _, err := models.GetPolochonByID(e.Database, data.PolochonID)
if err := user.GetConfig("polochon", &polochonConfig); err != nil { if err != nil {
return err return e.RenderError(w, fmt.Errorf("Could not find such polochon"))
} }
polochonConfig.URL = data.PolochonURL user.PolochonID = sql.NullString{
polochonConfig.Token = data.PolochonToken String: data.PolochonID,
if err := user.SetConfig("polochon", polochonConfig); err != nil { Valid: true,
return err }
user.PolochonActivated = false
} }
user.Token = data.PolochonToken
// Save the user with the new configurations // Save the user with the new configurations
if err := user.Update(e.Database); err != nil { if err := user.Update(e.Database); err != nil {
return err return err

View File

@ -15,13 +15,22 @@ import (
) )
const ( 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;` getUserQuery = `SELECT * FROM users WHERE name=$1;`
getUserByIDQuery = `SELECT * FROM users WHERE id=$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 *;` updateUserQuery = `UPDATE users SET
deleteUseQuery = `DELETE FROM users WHERE id=:id;` 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 ( const (
@ -31,17 +40,20 @@ const (
AdminRole = "admin" 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") var ErrUnknownUser = fmt.Errorf("users: user does'nt exist")
// User represents an user // User represents an user
type User struct { type User struct {
sqly.BaseModel sqly.BaseModel
Name string Name string `json:"name"`
Hash string Hash string `json:"-"`
Admin bool Admin bool `json:"admin"`
Activated bool RawConfig types.JSONText `json:"raw_config"`
RawConfig types.JSONText 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 // GetConfig unmarshal json from specified config key into v
@ -149,10 +161,20 @@ func GetAll(db *sqlx.DB) ([]*User, error) {
return users, nil 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 // Add user to database or raises an error
func (u *User) Add(q sqlx.Queryer) error { func (u *User) Add(q sqlx.Queryer) error {
var id string 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 { if err != nil {
return err return err
} }
@ -174,7 +196,7 @@ func (u *User) Update(ex *sqlx.DB) error {
// Delete user from database or raise an error // Delete user from database or raise an error
func (u *User) Delete(ex *sqlx.DB) error { func (u *User) Delete(ex *sqlx.DB) error {
_, err := ex.NamedExec(deleteUseQuery, u) _, err := ex.NamedExec(deleteUserQuery, u)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,3 +1,4 @@
DROP TABLE movies_tracked;
DROP TABLE movies; DROP TABLE movies;
DROP TABLE shows_tracked; DROP TABLE shows_tracked;
DROP TABLE episodes; DROP TABLE episodes;

View File

@ -1,3 +1,3 @@
ALTER TABLE external_medias ALTER COLUMN category TYPE media_category USING category::media_category; 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 source TYPE media_source USING source::media_source;
ALTER TABLE external_medias ALTER COLUMN type TYPE media_category USING type::media_type; ALTER TABLE external_medias ALTER COLUMN type TYPE media_type USING type::media_type;

View File

@ -1 +1,8 @@
DROP TABLE tokens; 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();

View File

@ -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;

View File

@ -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();