From 335d573d239185baf4709b7be957689c8a34cb19 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Thu, 1 Oct 2015 09:54:21 -0700 Subject: [PATCH] Restructuring into packages. --- Godeps/Godeps.json | 3 - api/characteristics.go | 189 ++++++++++++ compare.go => api/compare.go | 13 +- api/entities.go | 28 ++ api/measurements.go | 134 +++++++++ api/species.go | 150 ++++++++++ api/strains.go | 212 ++++++++++++++ api/users.go | 255 ++++++++++++++++ auth/claims.go | 44 +++ characteristics.go | 435 ---------------------------- entities.go | 28 -- handlers.go => handlers/handlers.go | 164 ++++------- helpers.go => helpers/helpers.go | 32 +- main.go | 32 +- measurements.go | 375 ------------------------ models/characteristics.go | 236 +++++++++++++++ models/database.go | 8 + models/measurements.go | 234 +++++++++++++++ models/species.go | 174 +++++++++++ models/strains.go | 184 ++++++++++++ models/users.go | 156 ++++++++++ payloads/payloads.go | 102 +++++++ species.go | 330 --------------------- strains.go | 406 -------------------------- types/claims.go | 11 + types/entities.go | 5 + types.go => types/types.go | 8 +- users.go | 392 ------------------------- 28 files changed, 2232 insertions(+), 2108 deletions(-) create mode 100644 api/characteristics.go rename compare.go => api/compare.go (88%) create mode 100644 api/entities.go create mode 100644 api/measurements.go create mode 100644 api/species.go create mode 100644 api/strains.go create mode 100644 api/users.go create mode 100644 auth/claims.go delete mode 100644 characteristics.go delete mode 100644 entities.go rename handlers.go => handlers/handlers.go (60%) rename helpers.go => helpers/helpers.go (71%) delete mode 100644 measurements.go create mode 100644 models/characteristics.go create mode 100644 models/database.go create mode 100644 models/measurements.go create mode 100644 models/species.go create mode 100644 models/strains.go create mode 100644 models/users.go create mode 100644 payloads/payloads.go delete mode 100644 species.go delete mode 100644 strains.go create mode 100644 types/claims.go create mode 100644 types/entities.go rename types.go => types/types.go (97%) delete mode 100644 users.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 1dcd5db..7644a80 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,9 +1,6 @@ { "ImportPath": "github.com/thermokarst/bactdb", "GoVersion": "go1.5", - "Packages": [ - "./..." - ], "Deps": [ { "ImportPath": "github.com/DavidHuie/gomigrate", diff --git a/api/characteristics.go b/api/characteristics.go new file mode 100644 index 0000000..275c9df --- /dev/null +++ b/api/characteristics.go @@ -0,0 +1,189 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/models" + "github.com/thermokarst/bactdb/payloads" + "github.com/thermokarst/bactdb/types" +) + +type CharacteristicService struct{} + +func (c CharacteristicService) Unmarshal(b []byte) (types.Entity, error) { + var cj payloads.CharacteristicPayload + err := json.Unmarshal(b, &cj) + return &cj, err +} + +func (c CharacteristicService) List(val *url.Values, claims *types.Claims) (types.Entity, *types.AppError) { + if val == nil { + return nil, helpers.ErrMustProvideOptionsJSON + } + var opt helpers.ListOptions + if err := helpers.SchemaDecoder.Decode(&opt, *val); err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristics, err := models.ListCharacteristics(opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains_opt, err := models.StrainOptsFromCharacteristics(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains, err := models.ListStrains(*strains_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species_opt, err := models.SpeciesOptsFromStrains(*strains_opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.ListSpecies(*species_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + measurements_opt, err := models.MeasurementOptsFromCharacteristics(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + measurements, err := models.ListMeasurements(*measurements_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + payload := payloads.CharacteristicsPayload{ + Characteristics: characteristics, + Measurements: measurements, + Strains: strains, + Species: species, + Meta: &models.CharacteristicMeta{ + CanAdd: helpers.CanAdd(claims), + }, + } + + return &payload, nil +} + +func (c CharacteristicService) Get(id int64, genus string, claims *types.Claims) (types.Entity, *types.AppError) { + characteristic, err := models.GetCharacteristic(id, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains, strain_opts, err := models.StrainsFromCharacteristicId(id, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species_opt, err := models.SpeciesOptsFromStrains(*strain_opts) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.ListSpecies(*species_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + measurements, _, err := models.MeasurementsFromCharacteristicId(id, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + payload := payloads.CharacteristicPayload{ + Characteristic: characteristic, + Measurements: measurements, + Strains: strains, + Species: species, + } + + return &payload, nil +} + +func (c CharacteristicService) Update(id int64, e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.CharacteristicPayload) + payload.Characteristic.UpdatedBy = claims.Sub + payload.Characteristic.Id = id + + // First, handle Characteristic Type + id, err := models.InsertOrGetCharacteristicType(payload.Characteristic.CharacteristicType, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + payload.Characteristic.CanEdit = helpers.CanEdit(claims, payload.Characteristic.CreatedBy) + + payload.Characteristic.CharacteristicTypeId = id + // TODO: fix this + count, err := models.DBH.Update(payload.Characteristic.CharacteristicBase) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + if count != 1 { + // TODO: fix this + return types.NewJSONError(models.ErrCharacteristicNotUpdated, http.StatusBadRequest) + } + + strains, strain_opts, err := models.StrainsFromCharacteristicId(id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + species_opt, err := models.SpeciesOptsFromStrains(*strain_opts) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.ListSpecies(*species_opt, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + payload.Strains = strains + // TODO: tack on measurements + payload.Measurements = nil + payload.Species = species + + return nil +} + +func (c CharacteristicService) Create(e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.CharacteristicPayload) + payload.Characteristic.CreatedBy = claims.Sub + payload.Characteristic.UpdatedBy = claims.Sub + + id, err := models.InsertOrGetCharacteristicType(payload.Characteristic.CharacteristicType, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + payload.Characteristic.CharacteristicTypeId = id + + // TODO: fix this + err = models.DBH.Insert(payload.Characteristic.CharacteristicBase) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristic, err := models.GetCharacteristic(payload.Characteristic.Id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + payload.Characteristic = characteristic + payload.Meta = &models.CharacteristicMeta{ + CanAdd: helpers.CanAdd(claims), + } + return nil +} diff --git a/compare.go b/api/compare.go similarity index 88% rename from compare.go rename to api/compare.go index 53d861e..eb47acc 100644 --- a/compare.go +++ b/api/compare.go @@ -1,4 +1,4 @@ -package main +package api import ( "bytes" @@ -11,9 +11,12 @@ import ( "time" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/gorilla/mux" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/payloads" + "github.com/thermokarst/bactdb/types" ) -func handleCompare(w http.ResponseWriter, r *http.Request) *appError { +func HandleCompare(w http.ResponseWriter, r *http.Request) *types.AppError { // types type Comparisions map[string]map[string]string type ComparisionsJSON [][]string @@ -23,7 +26,7 @@ func handleCompare(w http.ResponseWriter, r *http.Request) *appError { if mimeType == "" { mimeType = "json" } - claims := getClaims(r) + claims := helpers.GetClaims(r) var header string var data []byte @@ -33,11 +36,11 @@ func handleCompare(w http.ResponseWriter, r *http.Request) *appError { opt.Del("mimeType") opt.Del("token") opt.Add("Genus", mux.Vars(r)["genus"]) - measurementsEntity, appErr := measService.list(&opt, &claims) + measurementsEntity, appErr := measService.List(&opt, &claims) if appErr != nil { return appErr } - measurementsPayload := (measurementsEntity).(*MeasurementsPayload) + measurementsPayload := (measurementsEntity).(*payloads.MeasurementsPayload) // Assemble matrix characteristic_ids := strings.Split(opt.Get("characteristic_ids"), ",") diff --git a/api/entities.go b/api/entities.go new file mode 100644 index 0000000..9b86dc5 --- /dev/null +++ b/api/entities.go @@ -0,0 +1,28 @@ +package api + +import ( + "net/url" + + "github.com/thermokarst/bactdb/types" +) + +type Getter interface { + Get(int64, string, *types.Claims) (types.Entity, *types.AppError) +} + +type Lister interface { + List(*url.Values, *types.Claims) (types.Entity, *types.AppError) +} + +type Updater interface { + Update(int64, *types.Entity, string, *types.Claims) *types.AppError + Unmarshal([]byte) (types.Entity, error) +} + +type Creater interface { + Create(*types.Entity, string, *types.Claims) *types.AppError + Unmarshal([]byte) (types.Entity, error) +} +type Deleter interface { + Delete(int64, string, *types.Claims) *types.AppError +} diff --git a/api/measurements.go b/api/measurements.go new file mode 100644 index 0000000..53019bb --- /dev/null +++ b/api/measurements.go @@ -0,0 +1,134 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/models" + "github.com/thermokarst/bactdb/payloads" + "github.com/thermokarst/bactdb/types" +) + +type MeasurementService struct{} + +func (s MeasurementService) Unmarshal(b []byte) (types.Entity, error) { + var mj payloads.MeasurementPayload + err := json.Unmarshal(b, &mj) + return &mj, err +} + +func (m MeasurementService) List(val *url.Values, claims *types.Claims) (types.Entity, *types.AppError) { + if val == nil { + return nil, helpers.ErrMustProvideOptionsJSON + } + var opt helpers.MeasurementListOptions + if err := helpers.SchemaDecoder.Decode(&opt, *val); err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + measurements, err := models.ListMeasurements(opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + char_opts, err := models.CharacteristicOptsFromMeasurements(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristics, err := models.ListCharacteristics(*char_opts, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strain_opts, err := models.StrainOptsFromMeasurements(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains, err := models.ListStrains(*strain_opts, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + payload := payloads.MeasurementsPayload{ + Characteristics: characteristics, + Strains: strains, + Measurements: measurements, + } + + return &payload, nil +} + +func (m MeasurementService) Get(id int64, genus string, claims *types.Claims) (types.Entity, *types.AppError) { + measurement, err := models.GetMeasurement(id, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + payload := payloads.MeasurementPayload{ + Measurement: measurement, + } + + return &payload, nil +} + +func (s MeasurementService) Update(id int64, e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.MeasurementPayload) + payload.Measurement.UpdatedBy = claims.Sub + payload.Measurement.Id = id + + if payload.Measurement.TextMeasurementType.Valid { + id, err := models.GetTextMeasurementTypeId(payload.Measurement.TextMeasurementType.String) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + payload.Measurement.TextMeasurementTypeId.Int64 = id + payload.Measurement.TextMeasurementTypeId.Valid = true + } + + // TODO: fix this + count, err := models.DBH.Update(payload.Measurement.MeasurementBase) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + if count != 1 { + // TODO: fix this + return types.NewJSONError(models.ErrStrainNotUpdated, http.StatusBadRequest) + } + + measurement, err := models.GetMeasurement(id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + payload.Measurement = measurement + + return nil +} + +func (m MeasurementService) Delete(id int64, genus string, claims *types.Claims) *types.AppError { + q := `DELETE FROM measurements WHERE id=$1;` + // TODO: fix this + _, err := models.DBH.Exec(q, id) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + return nil +} + +func (m MeasurementService) Create(e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.MeasurementPayload) + payload.Measurement.CreatedBy = claims.Sub + payload.Measurement.UpdatedBy = claims.Sub + + // TODO: fix this + if err := models.DBH.Insert(payload.Measurement.MeasurementBase); err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + return nil + +} diff --git a/api/species.go b/api/species.go new file mode 100644 index 0000000..5df6b66 --- /dev/null +++ b/api/species.go @@ -0,0 +1,150 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/models" + "github.com/thermokarst/bactdb/payloads" + "github.com/thermokarst/bactdb/types" +) + +type SpeciesService struct{} + +func (s SpeciesService) Unmarshal(b []byte) (types.Entity, error) { + var sj payloads.SpeciesPayload + err := json.Unmarshal(b, &sj) + return &sj, err +} + +func (s SpeciesService) List(val *url.Values, claims *types.Claims) (types.Entity, *types.AppError) { + if val == nil { + return nil, helpers.ErrMustProvideOptionsJSON + } + var opt helpers.ListOptions + if err := helpers.SchemaDecoder.Decode(&opt, *val); err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.ListSpecies(opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains_opt, err := models.StrainOptsFromSpecies(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains, err := models.ListStrains(*strains_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + payload := payloads.ManySpeciesPayload{ + Species: species, + Strains: strains, + Meta: &models.SpeciesMeta{ + CanAdd: helpers.CanAdd(claims), + }, + } + + return &payload, nil +} + +func (s SpeciesService) Get(id int64, genus string, claims *types.Claims) (types.Entity, *types.AppError) { + species, err := models.GetSpecies(id, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains, err := models.StrainsFromSpeciesId(id, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + payload := payloads.SpeciesPayload{ + Species: species, + Strains: strains, + Meta: &models.SpeciesMeta{ + CanAdd: helpers.CanAdd(claims), + }, + } + + return &payload, nil +} + +func (s SpeciesService) Update(id int64, e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.SpeciesPayload) + payload.Species.UpdatedBy = claims.Sub + payload.Species.Id = id + + genus_id, err := models.GenusIdFromName(genus) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + payload.Species.SpeciesBase.GenusID = genus_id + + // TODO: fix this + count, err := models.DBH.Update(payload.Species.SpeciesBase) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + if count != 1 { + // TODO: fix this + return types.NewJSONError(models.ErrSpeciesNotUpdated, http.StatusBadRequest) + } + + // Reload to send back down the wire + species, err := models.GetSpecies(id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + strains, err := models.StrainsFromSpeciesId(id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + payload.Species = species + payload.Strains = strains + payload.Meta = &models.SpeciesMeta{ + CanAdd: helpers.CanAdd(claims), + } + + return nil +} + +func (s SpeciesService) Create(e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.SpeciesPayload) + payload.Species.CreatedBy = claims.Sub + payload.Species.UpdatedBy = claims.Sub + + genus_id, err := models.GenusIdFromName(genus) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + payload.Species.SpeciesBase.GenusID = genus_id + + // TODO: fix this + err = models.DBH.Insert(payload.Species.SpeciesBase) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + // Reload to send back down the wire + species, err := models.GetSpecies(payload.Species.Id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + // Note, no strains when new species + + payload.Species = species + payload.Meta = &models.SpeciesMeta{ + CanAdd: helpers.CanAdd(claims), + } + return nil +} diff --git a/api/strains.go b/api/strains.go new file mode 100644 index 0000000..2f61e4e --- /dev/null +++ b/api/strains.go @@ -0,0 +1,212 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/models" + "github.com/thermokarst/bactdb/payloads" + "github.com/thermokarst/bactdb/types" +) + +type StrainService struct{} + +func (s StrainService) Unmarshal(b []byte) (types.Entity, error) { + var sj payloads.StrainPayload + err := json.Unmarshal(b, &sj) + return &sj, err +} + +func (s StrainService) List(val *url.Values, claims *types.Claims) (types.Entity, *types.AppError) { + if val == nil { + return nil, helpers.ErrMustProvideOptionsJSON + } + var opt helpers.ListOptions + if err := helpers.SchemaDecoder.Decode(&opt, *val); err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + strains, err := models.ListStrains(opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species_opt, err := models.SpeciesOptsFromStrains(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.ListSpecies(*species_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristics_opt, err := models.CharacteristicsOptsFromStrains(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristics, err := models.ListCharacteristics(*characteristics_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristic_ids := []int64{} + for _, c := range *characteristics { + characteristic_ids = append(characteristic_ids, c.Id) + } + + strain_ids := []int64{} + for _, s := range *strains { + strain_ids = append(strain_ids, s.Id) + } + + measurement_opt := helpers.MeasurementListOptions{ + ListOptions: helpers.ListOptions{ + Genus: opt.Genus, + }, + Strains: strain_ids, + Characteristics: characteristic_ids, + } + + measurements, err := models.ListMeasurements(measurement_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + payload := payloads.StrainsPayload{ + Strains: strains, + Species: species, + Measurements: measurements, + Characteristics: characteristics, + Meta: &models.StrainMeta{ + CanAdd: helpers.CanAdd(claims), + }, + } + + return &payload, nil +} + +func (s StrainService) Get(id int64, genus string, claims *types.Claims) (types.Entity, *types.AppError) { + strain, err := models.GetStrain(id, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.GetSpecies(strain.SpeciesId, genus, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + opt := helpers.ListOptions{Genus: genus, Ids: []int64{id}} + characteristics_opt, err := models.CharacteristicsOptsFromStrains(opt) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristics, err := models.ListCharacteristics(*characteristics_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + characteristic_ids := []int64{} + for _, c := range *characteristics { + characteristic_ids = append(characteristic_ids, c.Id) + } + + measurement_opt := helpers.MeasurementListOptions{ + ListOptions: helpers.ListOptions{ + Genus: genus, + }, + Strains: []int64{id}, + Characteristics: characteristic_ids, + } + + measurements, err := models.ListMeasurements(measurement_opt, claims) + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + var many_species models.ManySpecies = []*models.Species{species} + + payload := payloads.StrainPayload{ + Strain: strain, + Species: &many_species, + Characteristics: characteristics, + Measurements: measurements, + Meta: &models.StrainMeta{ + CanAdd: helpers.CanAdd(claims), + }, + } + + return &payload, nil +} + +func (s StrainService) Update(id int64, e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.StrainPayload) + payload.Strain.UpdatedBy = claims.Sub + payload.Strain.Id = id + + // TODO: fix this + count, err := models.DBH.Update(payload.Strain.StrainBase) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + if count != 1 { + // TODO: fix this + return types.NewJSONError(models.ErrStrainNotUpdated, http.StatusBadRequest) + } + + strain, err := models.GetStrain(id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.GetSpecies(strain.SpeciesId, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + var many_species models.ManySpecies = []*models.Species{species} + + payload.Strain = strain + payload.Species = &many_species + payload.Meta = &models.StrainMeta{ + CanAdd: helpers.CanAdd(claims), + } + + return nil +} + +func (s StrainService) Create(e *types.Entity, genus string, claims *types.Claims) *types.AppError { + payload := (*e).(*payloads.StrainPayload) + payload.Strain.CreatedBy = claims.Sub + payload.Strain.UpdatedBy = claims.Sub + + // TODO: fix this + if err := models.DBH.Insert(payload.Strain.StrainBase); err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + strain, err := models.GetStrain(payload.Strain.Id, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + species, err := models.GetSpecies(strain.SpeciesId, genus, claims) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + var many_species models.ManySpecies = []*models.Species{species} + + payload.Strain = strain + payload.Species = &many_species + payload.Meta = &models.StrainMeta{ + CanAdd: helpers.CanAdd(claims), + } + + return nil +} diff --git a/api/users.go b/api/users.go new file mode 100644 index 0000000..b09963c --- /dev/null +++ b/api/users.go @@ -0,0 +1,255 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/url" + + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/gorilla/mux" + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/lib/pq" + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/mailgun/mailgun-go" + "github.com/thermokarst/bactdb/Godeps/_workspace/src/golang.org/x/crypto/bcrypt" + "github.com/thermokarst/bactdb/auth" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/models" + "github.com/thermokarst/bactdb/payloads" + "github.com/thermokarst/bactdb/types" +) + +var ( + // TODO: fix this + ErrUserNotFoundJSON = types.NewJSONError(models.ErrUserNotFound, http.StatusNotFound) + ErrUserNotUpdatedJSON = types.NewJSONError(models.ErrUserNotUpdated, http.StatusBadRequest) + ErrEmailAddressTakenJSON = types.NewJSONError(models.ErrEmailAddressTaken, http.StatusBadRequest) + MgAccts = make(map[string]mailgun.Mailgun) +) + +type UserService struct{} + +func (u UserService) Unmarshal(b []byte) (types.Entity, error) { + var uj payloads.UserPayload + err := json.Unmarshal(b, &uj) + return &uj, err +} + +func (u UserService) List(val *url.Values, claims *types.Claims) (types.Entity, *types.AppError) { + if val == nil { + return nil, helpers.ErrMustProvideOptionsJSON + } + var opt helpers.ListOptions + if err := helpers.SchemaDecoder.Decode(&opt, *val); err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + // TODO: fix this + users := make(models.Users, 0) + sql := `SELECT id, email, 'password' AS password, name, role, + created_at, updated_at, deleted_at + FROM users + WHERE verified IS TRUE + AND deleted_at IS NULL;` + if err := models.DBH.Select(&users, sql); err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + return &users, nil +} + +func (u UserService) Get(id int64, dummy string, claims *types.Claims) (types.Entity, *types.AppError) { + user, err := models.DbGetUserById(id) + user.Password = "" + if err != nil { + return nil, types.NewJSONError(err, http.StatusInternalServerError) + } + + user.CanEdit = claims.Role == "A" || id == claims.Sub + + payload := payloads.UserPayload{ + User: user, + Meta: &models.UserMeta{ + CanAdd: claims.Role == "A", + }, + } + return &payload, nil +} + +func (u UserService) Update(id int64, e *types.Entity, dummy string, claims *types.Claims) *types.AppError { + user := (*e).(*payloads.UserPayload).User + + original_user, err := models.DbGetUserById(id) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + user.Id = id + user.Password = original_user.Password + user.Verified = original_user.Verified + user.UpdatedAt = helpers.CurrentTime() + + if err := user.Validate(); err != nil { + return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity} + } + + // TODO: fix this + count, err := models.DBH.Update(user) + user.Password = "" + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + if count != 1 { + return ErrUserNotUpdatedJSON + } + + return nil +} + +func (u UserService) Create(e *types.Entity, dummy string, claims *types.Claims) *types.AppError { + user := (*e).(*payloads.UserPayload).User + if err := user.Validate(); err != nil { + return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity} + } + ct := helpers.CurrentTime() + user.CreatedAt = ct + user.UpdatedAt = ct + hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + user.Password = string(hash) + user.Role = "R" + user.Verified = false + + // TODO: fix this + if err := models.DBH.Insert(user); err != nil { + if err, ok := err.(*pq.Error); ok { + if err.Code == "23505" { + return ErrEmailAddressTakenJSON + } + } + return types.NewJSONError(err, http.StatusInternalServerError) + } + + user.Password = "password" // don't want to send the hashed PW back to the client + + q := `INSERT INTO verification (user_id, nonce, referer, created_at) VALUES ($1, $2, $3, $4);` + // TODO: move helpers.GenerateNonce + nonce, err := helpers.GenerateNonce() + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + // TODO: fix this + _, err = models.DBH.Exec(q, user.Id, nonce, claims.Ref, ct) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + // Send out confirmation email + // TODO: clean this up + mg, ok := MgAccts[claims.Ref] + if ok { + sender := fmt.Sprintf("%s Admin ", mg.Domain(), mg.Domain()) + recipient := fmt.Sprintf("%s <%s>", user.Name, user.Email) + subject := fmt.Sprintf("New Account Confirmation - %s", mg.Domain()) + message := fmt.Sprintf("You are receiving this message because this email "+ + "address was used to sign up for an account at %s. Please visit this "+ + "URL to complete the sign up process: %s/users/new/verify/%s. If you "+ + "did not request an account, please disregard this message.", + mg.Domain(), claims.Ref, nonce) + m := mailgun.NewMessage(sender, subject, message, recipient) + _, _, err := mg.Send(m) + if err != nil { + log.Printf("%+v\n", err) + return types.NewJSONError(err, http.StatusInternalServerError) + } + } + + return nil +} + +func HandleUserVerify(w http.ResponseWriter, r *http.Request) *types.AppError { + // TODO: clean this up + nonce := mux.Vars(r)["Nonce"] + q := `SELECT user_id, referer FROM verification WHERE nonce=$1;` + + var ver struct { + User_id int64 + Referer string + } + if err := models.DBH.SelectOne(&ver, q, nonce); err != nil { + log.Print(err) + return types.NewJSONError(err, http.StatusInternalServerError) + } + + if ver.User_id == 0 { + return types.NewJSONError(errors.New("No user found"), http.StatusInternalServerError) + } + + var user models.User + if err := models.DBH.Get(&user, ver.User_id); err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + + user.UpdatedAt = helpers.CurrentTime() + user.Verified = true + + count, err := models.DBH.Update(&user) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + if count != 1 { + return types.NewJSONError(errors.New("Count 0"), http.StatusInternalServerError) + } + + q = `DELETE FROM verification WHERE user_id=$1;` + _, err = models.DBH.Exec(q, user.Id) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + fmt.Fprintln(w, `{"msg":"All set! Please log in."}`) + return nil +} + +func HandleUserLockout(w http.ResponseWriter, r *http.Request) *types.AppError { + email := r.FormValue("email") + if email == "" { + return types.NewJSONError(errors.New("missing email"), http.StatusInternalServerError) + } + token, err := auth.Middleware.CreateToken(email) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + origin := r.Header.Get("Origin") + hostUrl, err := url.Parse(origin) + if err != nil { + return types.NewJSONError(err, http.StatusInternalServerError) + } + hostUrl.Path += "/users/lockoutauthenticate" + params := url.Values{} + params.Add("token", token) + hostUrl.RawQuery = params.Encode() + + // Send out email + // TODO: clean this up + mg, ok := MgAccts[origin] + if ok { + sender := fmt.Sprintf("%s Admin ", mg.Domain(), mg.Domain()) + recipient := fmt.Sprintf("%s", email) + subject := fmt.Sprintf("Password Reset Request - %s", mg.Domain()) + message := fmt.Sprintf("You are receiving this message because this email "+ + "address was used in an account lockout request at %s. Please visit "+ + "this URL to complete the process: %s. If you did not request help "+ + "with a lockout, please disregard this message.", + mg.Domain(), hostUrl.String()) + m := mailgun.NewMessage(sender, subject, message, recipient) + _, _, err := mg.Send(m) + if err != nil { + log.Printf("%+v\n", err) + return types.NewJSONError(err, http.StatusInternalServerError) + } + } + + fmt.Fprintln(w, `{}`) + return nil +} diff --git a/auth/claims.go b/auth/claims.go new file mode 100644 index 0000000..eac6424 --- /dev/null +++ b/auth/claims.go @@ -0,0 +1,44 @@ +package auth + +import ( + "os" + "time" + + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/thermokarst/jwt" + "github.com/thermokarst/bactdb/models" +) + +var ( + Middleware *jwt.Middleware + Config *jwt.Config = &jwt.Config{ + Secret: os.Getenv("SECRET"), + Auth: models.DbAuthenticate, + Claims: claimsFunc, + } +) + +func claimsFunc(email string) (map[string]interface{}, error) { + // TODO: use helper + currentTime := time.Now() + user, err := models.DbGetUserByEmail(email) + if err != nil { + return nil, err + } + return map[string]interface{}{ + "name": user.Name, + "iss": "bactdb", + "sub": user.Id, + "role": user.Role, + "iat": currentTime.Unix(), + "exp": currentTime.Add(time.Minute * 60).Unix(), + "ref": "", + }, nil +} + +func init() { + var err error + Middleware, err = jwt.New(Config) + if err != nil { + panic(err) + } +} diff --git a/characteristics.go b/characteristics.go deleted file mode 100644 index 0535fd7..0000000 --- a/characteristics.go +++ /dev/null @@ -1,435 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" -) - -var ( - ErrCharacteristicNotFound = errors.New("Characteristic not found") - ErrCharacteristicNotUpdated = errors.New("Characteristic not updated") -) - -func init() { - DB.AddTableWithName(CharacteristicBase{}, "characteristics").SetKeys(true, "Id") -} - -func (c *CharacteristicBase) PreInsert(e modl.SqlExecutor) error { - ct := currentTime() - c.CreatedAt = ct - c.UpdatedAt = ct - return nil -} - -func (c *CharacteristicBase) PreUpdate(e modl.SqlExecutor) error { - c.UpdatedAt = currentTime() - return nil -} - -type CharacteristicService struct{} - -type CharacteristicBase struct { - Id int64 `json:"id,omitempty"` - CharacteristicName string `db:"characteristic_name" json:"characteristicName"` - CharacteristicTypeId int64 `db:"characteristic_type_id" json:"-"` - SortOrder NullInt64 `db:"sort_order" json:"sortOrder"` - CreatedAt NullTime `db:"created_at" json:"createdAt"` - UpdatedAt NullTime `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` - CreatedBy int64 `db:"created_by" json:"createdBy"` - UpdatedBy int64 `db:"updated_by" json:"updatedBy"` - DeletedBy NullInt64 `db:"deleted_by" json:"deletedBy"` -} - -type Characteristic struct { - *CharacteristicBase - Measurements NullSliceInt64 `db:"measurements" json:"measurements"` - Strains NullSliceInt64 `db:"strains" json:"strains"` - CharacteristicType string `db:"characteristic_type_name" json:"characteristicTypeName"` - CanEdit bool `db:"-" json:"canEdit"` -} - -type Characteristics []*Characteristic - -type CharacteristicMeta struct { - CanAdd bool `json:"canAdd"` -} - -type CharacteristicPayload struct { - Characteristic *Characteristic `json:"characteristic"` - Measurements *Measurements `json:"measurements"` - Strains *Strains `json:"strains"` - Species *ManySpecies `json:"species"` - Meta *CharacteristicMeta `json:"meta"` -} - -type CharacteristicsPayload struct { - Characteristics *Characteristics `json:"characteristics"` - Measurements *Measurements `json:"measurements"` - Strains *Strains `json:"strains"` - Species *ManySpecies `json:"species"` - Meta *CharacteristicMeta `json:"meta"` -} - -func (c *CharacteristicPayload) marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c *CharacteristicsPayload) marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c CharacteristicService) unmarshal(b []byte) (entity, error) { - var cj CharacteristicPayload - err := json.Unmarshal(b, &cj) - return &cj, err -} - -func (c CharacteristicService) list(val *url.Values, claims *Claims) (entity, *appError) { - if val == nil { - return nil, ErrMustProvideOptionsJSON - } - var opt ListOptions - if err := schemaDecoder.Decode(&opt, *val); err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - characteristics, err := listCharacteristics(opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains_opt, err := strainOptsFromCharacteristics(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains, err := listStrains(*strains_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species_opt, err := speciesOptsFromStrains(*strains_opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species, err := listSpecies(*species_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - measurements_opt, err := measurementOptsFromCharacteristics(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - measurements, err := listMeasurements(*measurements_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - payload := CharacteristicsPayload{ - Characteristics: characteristics, - Measurements: measurements, - Strains: strains, - Species: species, - Meta: &CharacteristicMeta{ - CanAdd: canAdd(claims), - }, - } - - return &payload, nil -} - -func (c CharacteristicService) get(id int64, genus string, claims *Claims) (entity, *appError) { - characteristic, err := getCharacteristic(id, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains, strain_opts, err := strainsFromCharacteristicId(id, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species_opt, err := speciesOptsFromStrains(*strain_opts) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species, err := listSpecies(*species_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - measurements, _, err := measurementsFromCharacteristicId(id, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - payload := CharacteristicPayload{ - Characteristic: characteristic, - Measurements: measurements, - Strains: strains, - Species: species, - } - - return &payload, nil -} - -func (c CharacteristicService) update(id int64, e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*CharacteristicPayload) - payload.Characteristic.UpdatedBy = claims.Sub - payload.Characteristic.Id = id - - // First, handle Characteristic Type - id, err := insertOrGetCharacteristicType(payload.Characteristic.CharacteristicType, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - payload.Characteristic.CanEdit = canEdit(claims, payload.Characteristic.CreatedBy) - - payload.Characteristic.CharacteristicTypeId = id - count, err := DBH.Update(payload.Characteristic.CharacteristicBase) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - if count != 1 { - return newJSONError(ErrCharacteristicNotUpdated, http.StatusBadRequest) - } - - strains, strain_opts, err := strainsFromCharacteristicId(id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - species_opt, err := speciesOptsFromStrains(*strain_opts) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - species, err := listSpecies(*species_opt, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - payload.Strains = strains - // TODO: tack on measurements - payload.Measurements = nil - payload.Species = species - - return nil -} - -func (c CharacteristicService) create(e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*CharacteristicPayload) - payload.Characteristic.CreatedBy = claims.Sub - payload.Characteristic.UpdatedBy = claims.Sub - - id, err := insertOrGetCharacteristicType(payload.Characteristic.CharacteristicType, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - payload.Characteristic.CharacteristicTypeId = id - - err = DBH.Insert(payload.Characteristic.CharacteristicBase) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - characteristic, err := getCharacteristic(payload.Characteristic.Id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - payload.Characteristic = characteristic - payload.Meta = &CharacteristicMeta{ - CanAdd: canAdd(claims), - } - return nil -} - -func listCharacteristics(opt ListOptions, claims *Claims) (*Characteristics, error) { - var vals []interface{} - - q := `SELECT c.*, ct.characteristic_type_name, - array_agg(DISTINCT st.id) AS strains, array_agg(DISTINCT m.id) AS measurements - FROM strains st - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) - INNER JOIN measurements m ON m.strain_id=st.id - RIGHT OUTER JOIN characteristics c ON c.id=m.characteristic_id - INNER JOIN characteristic_types ct ON ct.id=c.characteristic_type_id` - vals = append(vals, opt.Genus) - - if len(opt.Ids) != 0 { - var counter int64 = 2 - w := valsIn("c.id", opt.Ids, &vals, &counter) - - q += fmt.Sprintf(" WHERE %s", w) - } - - q += ` GROUP BY c.id, ct.characteristic_type_name - ORDER BY ct.characteristic_type_name, c.sort_order ASC;` - - var characteristics Characteristics - err := DBH.Select(&characteristics, q, vals...) - if err != nil { - return nil, err - } - - for _, c := range characteristics { - c.CanEdit = canEdit(claims, c.CreatedBy) - } - - return &characteristics, nil -} - -func strainOptsFromCharacteristics(opt ListOptions) (*ListOptions, error) { - relatedStrainIds := make([]int64, 0) - baseQ := `SELECT DISTINCT m.strain_id - FROM measurements m - INNER JOIN strains st ON st.id=m.strain_id - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1)` - if opt.Ids == nil { - q := fmt.Sprintf("%s;", baseQ) - if err := DBH.Select(&relatedStrainIds, q, opt.Genus); err != nil { - return nil, err - } - } else { - var vals []interface{} - var count int64 = 2 - vals = append(vals, opt.Genus) - q := fmt.Sprintf("%s WHERE %s ", baseQ, valsIn("m.characteristic_id", opt.Ids, &vals, &count)) - - if err := DBH.Select(&relatedStrainIds, q, vals...); err != nil { - return nil, err - } - } - - return &ListOptions{Genus: opt.Genus, Ids: relatedStrainIds}, nil -} - -func measurementOptsFromCharacteristics(opt ListOptions) (*MeasurementListOptions, error) { - relatedMeasurementIds := make([]int64, 0) - baseQ := `SELECT m.id - FROM measurements m - INNER JOIN strains st ON st.id=m.strain_id - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1)` - - if opt.Ids == nil { - q := fmt.Sprintf("%s;", baseQ) - if err := DBH.Select(&relatedMeasurementIds, q, opt.Genus); err != nil { - return nil, err - } - } else { - var vals []interface{} - var count int64 = 2 - vals = append(vals, opt.Genus) - q := fmt.Sprintf("%s WHERE %s;", baseQ, valsIn("characteristic_id", opt.Ids, &vals, &count)) - - if err := DBH.Select(&relatedMeasurementIds, q, vals...); err != nil { - return nil, err - } - } - - return &MeasurementListOptions{ListOptions: ListOptions{Genus: opt.Genus, Ids: relatedMeasurementIds}, Strains: nil, Characteristics: nil}, nil -} - -func strainsFromCharacteristicId(id int64, genus string, claims *Claims) (*Strains, *ListOptions, error) { - opt := ListOptions{ - Genus: genus, - Ids: []int64{id}, - } - - strains_opt, err := strainOptsFromCharacteristics(opt) - if err != nil { - return nil, nil, err - } - - strains, err := listStrains(*strains_opt, claims) - if err != nil { - return nil, nil, err - } - - return strains, strains_opt, nil -} - -func measurementsFromCharacteristicId(id int64, genus string, claims *Claims) (*Measurements, *MeasurementListOptions, error) { - opt := ListOptions{ - Genus: genus, - Ids: []int64{id}, - } - - measurement_opt, err := measurementOptsFromCharacteristics(opt) - if err != nil { - return nil, nil, err - } - - measurements, err := listMeasurements(*measurement_opt, claims) - if err != nil { - return nil, nil, err - } - - return measurements, measurement_opt, nil -} - -func getCharacteristic(id int64, genus string, claims *Claims) (*Characteristic, error) { - var characteristic Characteristic - q := `SELECT c.*, ct.characteristic_type_name, - array_agg(DISTINCT st.id) AS strains, array_agg(DISTINCT m.id) AS measurements - FROM strains st - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) - INNER JOIN measurements m ON m.strain_id=st.id - RIGHT OUTER JOIN characteristics c ON c.id=m.characteristic_id - INNER JOIN characteristic_types ct ON ct.id=c.characteristic_type_id - WHERE c.id=$2 - GROUP BY c.id, ct.characteristic_type_name;` - if err := DBH.SelectOne(&characteristic, q, genus, id); err != nil { - if err == sql.ErrNoRows { - return nil, ErrCharacteristicNotFound - } - return nil, err - } - - characteristic.CanEdit = canEdit(claims, characteristic.CreatedBy) - - return &characteristic, nil -} - -func insertOrGetCharacteristicType(val string, claims *Claims) (int64, error) { - var id int64 - q := `SELECT id FROM characteristic_types WHERE characteristic_type_name=$1;` - if err := DBH.SelectOne(&id, q, val); err != nil { - if err == sql.ErrNoRows { - i := `INSERT INTO characteristic_types - (characteristic_type_name, created_at, updated_at, created_by, updated_by) - VALUES ($1, $2, $3, $4, $5) RETURNING id;` - ct := currentTime() - var result sql.Result - var insertErr error - stmt, err := DB.Db.Prepare(i) - if result, insertErr = stmt.Exec(val, ct, ct, claims.Sub, claims.Sub); insertErr != nil { - return 0, insertErr - } - id, err = result.LastInsertId() - if err != nil { - return 0, err - } - } else { - return 0, err - } - } - return id, nil -} diff --git a/entities.go b/entities.go deleted file mode 100644 index 6165044..0000000 --- a/entities.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import "net/url" - -type entity interface { - marshal() ([]byte, error) -} - -type getter interface { - get(int64, string, *Claims) (entity, *appError) -} - -type lister interface { - list(*url.Values, *Claims) (entity, *appError) -} - -type updater interface { - update(int64, *entity, string, *Claims) *appError - unmarshal([]byte) (entity, error) -} - -type creater interface { - create(*entity, string, *Claims) *appError - unmarshal([]byte) (entity, error) -} -type deleter interface { - delete(int64, string, *Claims) *appError -} diff --git a/handlers.go b/handlers/handlers.go similarity index 60% rename from handlers.go rename to handlers/handlers.go index ca35467..167d96a 100644 --- a/handlers.go +++ b/handlers/handlers.go @@ -1,4 +1,4 @@ -package main +package handlers import ( "encoding/json" @@ -16,26 +16,17 @@ import ( "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/gorilla/mux" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/nytimes/gziphandler" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/thermokarst/jwt" + "github.com/thermokarst/bactdb/api" + "github.com/thermokarst/bactdb/auth" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/models" + "github.com/thermokarst/bactdb/types" ) -var ( - config *jwt.Config - j *jwt.Middleware -) - -type Claims struct { - Name string - Iss string - Sub int64 - Role string - Iat int64 - Exp int64 - Ref string -} - func verifyClaims(claims []byte, r *http.Request) error { + // TODO: use helper currentTime := time.Now() - var c Claims + var c types.Claims err := json.Unmarshal(claims, &c) if err != nil { return err @@ -48,53 +39,24 @@ func verifyClaims(claims []byte, r *http.Request) error { } func Handler() http.Handler { - claimsFunc := func(email string) (map[string]interface{}, error) { - currentTime := time.Now() - user, err := dbGetUserByEmail(email) - if err != nil { - return nil, err - } - return map[string]interface{}{ - "name": user.Name, - "iss": "bactdb", - "sub": user.Id, - "role": user.Role, - "iat": currentTime.Unix(), - "exp": currentTime.Add(time.Minute * 60).Unix(), - "ref": "", - }, nil - } - - config = &jwt.Config{ - Secret: os.Getenv("SECRET"), - Auth: dbAuthenticate, - Claims: claimsFunc, - } - - var err error - j, err = jwt.New(config) - if err != nil { - panic(err) - } - m := mux.NewRouter() - userService := UserService{} - strainService := StrainService{} - speciesService := SpeciesService{} - characteristicService := CharacteristicService{} - measurementService := MeasurementService{} + userService := api.UserService{} + strainService := api.StrainService{} + speciesService := api.SpeciesService{} + characteristicService := api.CharacteristicService{} + measurementService := api.MeasurementService{} - m.Handle("/authenticate", tokenHandler(j.Authenticate())).Methods("POST") - m.Handle("/refresh", j.Secure(errorHandler(tokenRefresh(j)), verifyClaims)).Methods("POST") + m.Handle("/authenticate", tokenHandler(auth.Middleware.Authenticate())).Methods("POST") + m.Handle("/refresh", auth.Middleware.Secure(errorHandler(tokenRefresh(auth.Middleware)), verifyClaims)).Methods("POST") // Everything past here is lumped under a genus s := m.PathPrefix("/{genus}").Subrouter() s.Handle("/users", errorHandler(handleCreater(userService))).Methods("POST") - s.Handle("/users/verify/{Nonce}", errorHandler(handleUserVerify)).Methods("GET") - s.Handle("/users/lockout", errorHandler(handleUserLockout)).Methods("POST") + s.Handle("/users/verify/{Nonce}", errorHandler(api.HandleUserVerify)).Methods("GET") + s.Handle("/users/lockout", errorHandler(api.HandleUserLockout)).Methods("POST") - s.Handle("/compare", j.Secure(errorHandler(handleCompare), verifyClaims)).Methods("GET") + s.Handle("/compare", auth.Middleware.Secure(errorHandler(api.HandleCompare), verifyClaims)).Methods("GET") type r struct { f errorHandler @@ -127,126 +89,126 @@ func Handler() http.Handler { } for _, route := range routes { - s.Handle(route.p, j.Secure(errorHandler(route.f), verifyClaims)).Methods(route.m) + s.Handle(route.p, auth.Middleware.Secure(errorHandler(route.f), verifyClaims)).Methods(route.m) } return jsonHandler(gziphandler.GzipHandler(corsHandler(m))) } -func handleGetter(g getter) errorHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { +func handleGetter(g api.Getter) errorHandler { + return func(w http.ResponseWriter, r *http.Request) *types.AppError { id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } - claims := getClaims(r) + claims := helpers.GetClaims(r) - e, appErr := g.get(id, mux.Vars(r)["genus"], &claims) + e, appErr := g.Get(id, mux.Vars(r)["genus"], &claims) if appErr != nil { return appErr } - data, err := e.marshal() + data, err := e.Marshal() if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } w.Write(data) return nil } } -func handleLister(l lister) errorHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { +func handleLister(l api.Lister) errorHandler { + return func(w http.ResponseWriter, r *http.Request) *types.AppError { opt := r.URL.Query() opt.Add("Genus", mux.Vars(r)["genus"]) - claims := getClaims(r) + claims := helpers.GetClaims(r) - es, appErr := l.list(&opt, &claims) + es, appErr := l.List(&opt, &claims) if appErr != nil { return appErr } - data, err := es.marshal() + data, err := es.Marshal() if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } w.Write(data) return nil } } -func handleUpdater(u updater) errorHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { +func handleUpdater(u api.Updater) errorHandler { + return func(w http.ResponseWriter, r *http.Request) *types.AppError { id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } - e, err := u.unmarshal(bodyBytes) + e, err := u.Unmarshal(bodyBytes) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } - claims := getClaims(r) + claims := helpers.GetClaims(r) - appErr := u.update(id, &e, mux.Vars(r)["genus"], &claims) + appErr := u.Update(id, &e, mux.Vars(r)["genus"], &claims) if appErr != nil { return appErr } - data, err := e.marshal() + data, err := e.Marshal() if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } w.Write(data) return nil } } -func handleCreater(c creater) errorHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { +func handleCreater(c api.Creater) errorHandler { + return func(w http.ResponseWriter, r *http.Request) *types.AppError { bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } - e, err := c.unmarshal(bodyBytes) + e, err := c.Unmarshal(bodyBytes) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } - claims := getClaims(r) + claims := helpers.GetClaims(r) - appErr := c.create(&e, mux.Vars(r)["genus"], &claims) + appErr := c.Create(&e, mux.Vars(r)["genus"], &claims) if appErr != nil { return appErr } - data, err := e.marshal() + data, err := e.Marshal() if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } w.Write(data) return nil } } -func handleDeleter(d deleter) errorHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { +func handleDeleter(d api.Deleter) errorHandler { + return func(w http.ResponseWriter, r *http.Request) *types.AppError { id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } - claims := getClaims(r) + claims := helpers.GetClaims(r) - appErr := d.delete(id, mux.Vars(r)["genus"], &claims) + appErr := d.Delete(id, mux.Vars(r)["genus"], &claims) if appErr != nil { return appErr } @@ -320,7 +282,7 @@ func jsonHandler(h http.Handler) http.Handler { return http.HandlerFunc(j) } -type errorHandler func(http.ResponseWriter, *http.Request) *appError +type errorHandler func(http.ResponseWriter, *http.Request) *types.AppError func (fn errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := fn(w, r); err != nil { @@ -330,16 +292,16 @@ func (fn errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func tokenRefresh(j *jwt.Middleware) errorHandler { - t := func(w http.ResponseWriter, r *http.Request) *appError { - claims := getClaims(r) - user, err := dbGetUserById(claims.Sub) + t := func(w http.ResponseWriter, r *http.Request) *types.AppError { + claims := helpers.GetClaims(r) + user, err := models.DbGetUserById(claims.Sub) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } user.Password = "" - token, err := j.CreateToken(user.Email) + token, err := auth.Middleware.CreateToken(user.Email) if err != nil { - return newJSONError(err, http.StatusInternalServerError) + return types.NewJSONError(err, http.StatusInternalServerError) } data, _ := json.Marshal(struct { Token string `json:"token"` diff --git a/helpers.go b/helpers/helpers.go similarity index 71% rename from helpers.go rename to helpers/helpers.go index 57f50c0..a2e5500 100644 --- a/helpers.go +++ b/helpers/helpers.go @@ -1,4 +1,4 @@ -package main +package helpers import ( "crypto/rand" @@ -8,15 +8,18 @@ import ( "net/http" "time" + "github.com/gorilla/schema" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/gorilla/context" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/lib/pq" + "github.com/thermokarst/bactdb/types" ) var ( ErrMustProvideOptions = errors.New("Must provide necessary options") - ErrMustProvideOptionsJSON = newJSONError(ErrMustProvideOptions, http.StatusBadRequest) + ErrMustProvideOptionsJSON = types.NewJSONError(ErrMustProvideOptions, http.StatusBadRequest) StatusUnprocessableEntity = 422 MustProvideAValue = "Must provide a value" + SchemaDecoder = schema.NewDecoder() ) // ListOptions specifies general pagination options for fetching a list of results @@ -45,10 +48,16 @@ func (o ListOptions) PerPageOrDefault() int64 { return o.PerPage } +type MeasurementListOptions struct { + ListOptions + Strains []int64 `schema:"strain_ids"` + Characteristics []int64 `schema:"characteristic_ids"` +} + // DefaultPerPage is the default number of items to return in a paginated result set const DefaultPerPage = 10 -func valsIn(attribute string, values []int64, vals *[]interface{}, counter *int64) string { +func ValsIn(attribute string, values []int64, vals *[]interface{}, counter *int64) string { if len(values) == 1 { return fmt.Sprintf("%v=%v", attribute, values[0]) } @@ -63,8 +72,8 @@ func valsIn(attribute string, values []int64, vals *[]interface{}, counter *int6 return m } -func currentTime() NullTime { - return NullTime{ +func CurrentTime() types.NullTime { + return types.NullTime{ pq.NullTime{ Time: time.Now(), Valid: true, @@ -72,7 +81,8 @@ func currentTime() NullTime { } } -func generateNonce() (string, error) { +// TODO: move this +func GenerateNonce() (string, error) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { @@ -81,11 +91,11 @@ func generateNonce() (string, error) { return base64.URLEncoding.EncodeToString(b), nil } -func getClaims(r *http.Request) Claims { +func GetClaims(r *http.Request) types.Claims { con := context.Get(r, "claims") - var claims Claims + var claims types.Claims if con != nil { - claims = con.(Claims) + claims = con.(types.Claims) } origin := r.Header.Get("Origin") if origin != "" { @@ -94,10 +104,10 @@ func getClaims(r *http.Request) Claims { return claims } -func canAdd(claims *Claims) bool { +func CanAdd(claims *types.Claims) bool { return claims.Role == "A" || claims.Role == "W" } -func canEdit(claims *Claims, author int64) bool { +func CanEdit(claims *types.Claims, author int64) bool { return claims.Sub == author || claims.Role == "A" } diff --git a/main.go b/main.go index 1004d89..e52e2eb 100644 --- a/main.go +++ b/main.go @@ -10,18 +10,12 @@ import ( "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/DavidHuie/gomigrate" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/codegangsta/cli" - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/gorilla/schema" - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/sqlx" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/lib/pq" "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/mailgun/mailgun-go" -) - -var ( - DB = &modl.DbMap{Dialect: modl.PostgresDialect{}} - DBH modl.SqlExecutor = DB - schemaDecoder = schema.NewDecoder() - mgAccts = make(map[string]mailgun.Mailgun) + "github.com/thermokarst/bactdb/api" + "github.com/thermokarst/bactdb/handlers" + "github.com/thermokarst/bactdb/models" ) func main() { @@ -37,12 +31,12 @@ func main() { } else { connection += " sslmode=disable" } - DB.Dbx, err = sqlx.Open("postgres", connection) + models.DB.Dbx, err = sqlx.Open("postgres", connection) if err != nil { log.Fatal("Error connecting to PostgreSQL database (using PG* environment variables): ", err) } - DB.TraceOn("[modl]", log.New(os.Stdout, "bactdb:", log.Lmicroseconds)) - DB.Db = DB.Dbx.DB + models.DB.TraceOn("[modl]", log.New(os.Stdout, "bactdb:", log.Lmicroseconds)) + models.DB.Db = models.DB.Dbx.DB }) app := cli.NewApp() @@ -100,7 +94,7 @@ func cmdServe(c *cli.Context) { log.Printf("Mailgun: %+v", accounts) for _, a := range accounts { - mgAccts[a.Ref] = mailgun.NewMailgun(a.Domain, a.Private, a.Public) + api.MgAccts[a.Ref] = mailgun.NewMailgun(a.Domain, a.Private, a.Public) } addr := os.Getenv("PORT") @@ -110,7 +104,7 @@ func cmdServe(c *cli.Context) { httpAddr := fmt.Sprintf(":%v", addr) m := http.NewServeMux() - m.Handle("/api/", http.StripPrefix("/api", Handler())) + m.Handle("/api/", http.StripPrefix("/api", handlers.Handler())) log.Print("Listening on ", httpAddr) err = http.ListenAndServe(httpAddr, m) @@ -121,16 +115,17 @@ func cmdServe(c *cli.Context) { func cmdMigrateDb(c *cli.Context) { migrationsPath := c.String("migration_path") - migrator, err := gomigrate.NewMigrator(DB.Dbx.DB, gomigrate.Postgres{}, migrationsPath) + migrator, err := gomigrate.NewMigrator(models.DB.Dbx.DB, gomigrate.Postgres{}, migrationsPath) if err != nil { log.Fatal("Error initializing migrations: ", err) } - users := make(Users, 0) + users := make(models.Users, 0) if c.Bool("drop") { // Back up users table - if err := DBH.Select(&users, `SELECT * FROM users;`); err != nil { + // TODO: look into this + if err := models.DBH.Select(&users, `SELECT * FROM users;`); err != nil { log.Fatal("Couldn't back up identity tables: ", err) } log.Printf("%+v Users", len(users)) @@ -152,7 +147,8 @@ func cmdMigrateDb(c *cli.Context) { if len(users) > 0 { // varargs don't seem to work here, loop instead for _, user := range users { - if err := DBH.Insert(user); err != nil { + // TODO: look into this + if err := models.DBH.Insert(user); err != nil { log.Fatal("Couldn't restore user: ", err) } } diff --git a/measurements.go b/measurements.go deleted file mode 100644 index 5783fb4..0000000 --- a/measurements.go +++ /dev/null @@ -1,375 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" -) - -var ( - ErrMeasurementNotFound = errors.New("Measurement not found") - ErrMeasurementNotFoundJSON = newJSONError(ErrMeasurementNotFound, http.StatusNotFound) -) - -func init() { - DB.AddTableWithName(MeasurementBase{}, "measurements").SetKeys(true, "Id") -} - -func (m *MeasurementBase) PreInsert(e modl.SqlExecutor) error { - ct := currentTime() - m.CreatedAt = ct - m.UpdatedAt = ct - return nil -} - -func (m *MeasurementBase) PreUpdate(e modl.SqlExecutor) error { - m.UpdatedAt = currentTime() - return nil -} - -type MeasurementService struct{} - -// There are three types of supported measurements: fixed-text, free-text, -// & numerical. The table has a constraint that will allow at most one -// for a particular combination of strain & characteristic. -// MeasurementBase is what the DB expects to see for inserts/updates -type MeasurementBase struct { - Id int64 `json:"id,omitempty"` - StrainId int64 `db:"strain_id" json:"strain"` - CharacteristicId int64 `db:"characteristic_id" json:"characteristic"` - TextMeasurementTypeId NullInt64 `db:"text_measurement_type_id" json:"-"` - TxtValue NullString `db:"txt_value" json:"-"` - NumValue NullFloat64 `db:"num_value" json:"-"` - ConfidenceInterval NullFloat64 `db:"confidence_interval" json:"confidenceInterval"` - UnitTypeId NullInt64 `db:"unit_type_id" json:"-"` - Notes NullString `db:"notes" json:"notes"` - TestMethodId NullInt64 `db:"test_method_id" json:"-"` - CreatedAt NullTime `db:"created_at" json:"createdAt"` - UpdatedAt NullTime `db:"updated_at" json:"updatedAt"` - CreatedBy int64 `db:"created_by" json:"createdBy"` - UpdatedBy int64 `db:"updated_by" json:"updatedBy"` -} - -type Measurement struct { - *MeasurementBase - TextMeasurementType NullString `db:"text_measurement_type_name" json:"-"` - UnitType NullString `db:"unit_type_name" json:"unitType"` - TestMethod NullString `db:"test_method_name" json:"testMethod"` - CanEdit bool `db:"-" json:"canEdit"` -} - -type FakeMeasurement Measurement - -func (m *Measurement) MarshalJSON() ([]byte, error) { - fm := FakeMeasurement(*m) - return json.Marshal(struct { - *FakeMeasurement - Value string `json:"value"` - }{ - FakeMeasurement: &fm, - Value: m.Value(), - }) -} - -func (m *Measurement) UnmarshalJSON(b []byte) error { - var measurement struct { - FakeMeasurement - Value interface{} `json:"value"` - } - if err := json.Unmarshal(b, &measurement); err != nil { - return err - } - - switch v := measurement.Value.(type) { - case string: - // Test if actually a lookup - id, err := getTextMeasurementTypeId(v) - if err != nil { - if err == sql.ErrNoRows { - measurement.TxtValue = NullString{sql.NullString{String: v, Valid: true}} - } else { - return err - } - } else { - measurement.TextMeasurementTypeId = NullInt64{sql.NullInt64{Int64: id, Valid: true}} - } - case int64: - measurement.NumValue = NullFloat64{sql.NullFloat64{Float64: float64(v), Valid: true}} - case float64: - measurement.NumValue = NullFloat64{sql.NullFloat64{Float64: v, Valid: true}} - } - - *m = Measurement(measurement.FakeMeasurement) - - return nil -} - -func (m *Measurement) Value() string { - if m.TextMeasurementType.Valid { - return m.TextMeasurementType.String - } - if m.TxtValue.Valid { - return m.TxtValue.String - } - if m.NumValue.Valid { - return fmt.Sprintf("%f", m.NumValue.Float64) - } - return "" -} - -type Measurements []*Measurement - -type MeasurementMeta struct { - CanAdd bool `json:"canAdd"` -} - -type MeasurementPayload struct { - Measurement *Measurement `json:"measurement"` -} - -type MeasurementsPayload struct { - Strains *Strains `json:"strains"` - Characteristics *Characteristics `json:"characteristics"` - Measurements *Measurements `json:"measurements"` -} - -func (m *MeasurementPayload) marshal() ([]byte, error) { - return json.Marshal(m) -} - -func (m *MeasurementsPayload) marshal() ([]byte, error) { - return json.Marshal(m) -} - -func (s MeasurementService) unmarshal(b []byte) (entity, error) { - var mj MeasurementPayload - err := json.Unmarshal(b, &mj) - return &mj, err -} - -type MeasurementListOptions struct { - ListOptions - Strains []int64 `schema:"strain_ids"` - Characteristics []int64 `schema:"characteristic_ids"` -} - -func (m MeasurementService) list(val *url.Values, claims *Claims) (entity, *appError) { - if val == nil { - return nil, ErrMustProvideOptionsJSON - } - var opt MeasurementListOptions - if err := schemaDecoder.Decode(&opt, *val); err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - measurements, err := listMeasurements(opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - char_opts, err := characteristicOptsFromMeasurements(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - characteristics, err := listCharacteristics(*char_opts, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strain_opts, err := strainOptsFromMeasurements(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains, err := listStrains(*strain_opts, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - payload := MeasurementsPayload{ - Characteristics: characteristics, - Strains: strains, - Measurements: measurements, - } - - return &payload, nil -} - -func (m MeasurementService) get(id int64, genus string, claims *Claims) (entity, *appError) { - measurement, err := getMeasurement(id, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - payload := MeasurementPayload{ - Measurement: measurement, - } - - return &payload, nil -} - -func (s MeasurementService) update(id int64, e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*MeasurementPayload) - payload.Measurement.UpdatedBy = claims.Sub - payload.Measurement.Id = id - - if payload.Measurement.TextMeasurementType.Valid { - id, err := getTextMeasurementTypeId(payload.Measurement.TextMeasurementType.String) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - payload.Measurement.TextMeasurementTypeId.Int64 = id - payload.Measurement.TextMeasurementTypeId.Valid = true - } - - count, err := DBH.Update(payload.Measurement.MeasurementBase) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - if count != 1 { - return newJSONError(ErrStrainNotUpdated, http.StatusBadRequest) - } - - measurement, err := getMeasurement(id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - payload.Measurement = measurement - - return nil -} - -func (m MeasurementService) delete(id int64, genus string, claims *Claims) *appError { - q := `DELETE FROM measurements WHERE id=$1;` - _, err := DBH.Exec(q, id) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - return nil -} - -func (m MeasurementService) create(e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*MeasurementPayload) - payload.Measurement.CreatedBy = claims.Sub - payload.Measurement.UpdatedBy = claims.Sub - - if err := DBH.Insert(payload.Measurement.MeasurementBase); err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - return nil - -} - -func listMeasurements(opt MeasurementListOptions, claims *Claims) (*Measurements, error) { - var vals []interface{} - - q := `SELECT m.*, t.text_measurement_name AS text_measurement_type_name, - u.symbol AS unit_type_name, te.name AS test_method_name - FROM measurements m - INNER JOIN strains st ON st.id=m.strain_id - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=$1 - LEFT OUTER JOIN characteristics c ON c.id=m.characteristic_id - LEFT OUTER JOIN text_measurement_types t ON t.id=m.text_measurement_type_id - LEFT OUTER JOIN unit_types u ON u.id=m.unit_type_id - LEFT OUTER JOIN test_methods te ON te.id=m.test_method_id` - vals = append(vals, opt.Genus) - - strainIds := len(opt.Strains) != 0 - charIds := len(opt.Characteristics) != 0 - ids := len(opt.Ids) != 0 - - if strainIds || charIds || ids { - var paramsCounter int64 = 2 - q += "\nWHERE (" - - // Filter by strains - if strainIds { - q += valsIn("st.id", opt.Strains, &vals, ¶msCounter) - } - - if strainIds && (charIds || ids) { - q += " AND " - } - - // Filter by characteristics - if charIds { - q += valsIn("c.id", opt.Characteristics, &vals, ¶msCounter) - } - - if charIds && ids { - q += " AND " - } - - // Get specific records - if ids { - q += valsIn("m.id", opt.Ids, &vals, ¶msCounter) - } - q += ")" - } - q += ";" - - measurements := make(Measurements, 0) - err := DBH.Select(&measurements, q, vals...) - if err != nil { - return nil, err - } - - for _, m := range measurements { - m.CanEdit = canEdit(claims, m.CreatedBy) - } - - return &measurements, nil -} - -func getMeasurement(id int64, genus string, claims *Claims) (*Measurement, error) { - var measurement Measurement - - q := `SELECT m.*, t.text_measurement_name AS text_measurement_type_name, - u.symbol AS unit_type_name, te.name AS test_method_name - FROM measurements m - INNER JOIN strains st ON st.id=m.strain_id - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) - LEFT OUTER JOIN characteristics c ON c.id=m.characteristic_id - LEFT OUTER JOIN text_measurement_types t ON t.id=m.text_measurement_type_id - LEFT OUTER JOIN unit_types u ON u.id=m.unit_type_id - LEFT OUTER JOIN test_methods te ON te.id=m.test_method_id - WHERE m.id=$2;` - if err := DBH.SelectOne(&measurement, q, genus, id); err != nil { - if err == sql.ErrNoRows { - return nil, ErrMeasurementNotFound - } - return nil, err - } - - measurement.CanEdit = canEdit(claims, measurement.CreatedBy) - - return &measurement, nil -} - -func characteristicOptsFromMeasurements(opt MeasurementListOptions) (*ListOptions, error) { - return &ListOptions{Genus: opt.Genus, Ids: opt.Characteristics}, nil -} - -func strainOptsFromMeasurements(opt MeasurementListOptions) (*ListOptions, error) { - return &ListOptions{Genus: opt.Genus, Ids: opt.Strains}, nil -} - -func getTextMeasurementTypeId(val string) (int64, error) { - var id int64 - q := `SELECT id FROM text_measurement_types WHERE text_measurement_name=$1;` - - if err := DBH.SelectOne(&id, q, val); err != nil { - return 0, err - } - return id, nil -} diff --git a/models/characteristics.go b/models/characteristics.go new file mode 100644 index 0000000..a913c61 --- /dev/null +++ b/models/characteristics.go @@ -0,0 +1,236 @@ +package models + +import ( + "database/sql" + "errors" + "fmt" + + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/types" +) + +var ( + ErrCharacteristicNotFound = errors.New("Characteristic not found") + ErrCharacteristicNotUpdated = errors.New("Characteristic not updated") +) + +func init() { + DB.AddTableWithName(CharacteristicBase{}, "characteristics").SetKeys(true, "Id") +} + +func (c *CharacteristicBase) PreInsert(e modl.SqlExecutor) error { + ct := helpers.CurrentTime() + c.CreatedAt = ct + c.UpdatedAt = ct + return nil +} + +func (c *CharacteristicBase) PreUpdate(e modl.SqlExecutor) error { + c.UpdatedAt = helpers.CurrentTime() + return nil +} + +type CharacteristicBase struct { + Id int64 `json:"id,omitempty"` + CharacteristicName string `db:"characteristic_name" json:"characteristicName"` + CharacteristicTypeId int64 `db:"characteristic_type_id" json:"-"` + SortOrder types.NullInt64 `db:"sort_order" json:"sortOrder"` + CreatedAt types.NullTime `db:"created_at" json:"createdAt"` + UpdatedAt types.NullTime `db:"updated_at" json:"updatedAt"` + DeletedAt types.NullTime `db:"deleted_at" json:"deletedAt"` + CreatedBy int64 `db:"created_by" json:"createdBy"` + UpdatedBy int64 `db:"updated_by" json:"updatedBy"` + DeletedBy types.NullInt64 `db:"deleted_by" json:"deletedBy"` +} + +type Characteristic struct { + *CharacteristicBase + Measurements types.NullSliceInt64 `db:"measurements" json:"measurements"` + Strains types.NullSliceInt64 `db:"strains" json:"strains"` + CharacteristicType string `db:"characteristic_type_name" json:"characteristicTypeName"` + CanEdit bool `db:"-" json:"canEdit"` +} + +type Characteristics []*Characteristic + +type CharacteristicMeta struct { + CanAdd bool `json:"canAdd"` +} + +func ListCharacteristics(opt helpers.ListOptions, claims *types.Claims) (*Characteristics, error) { + var vals []interface{} + + q := `SELECT c.*, ct.characteristic_type_name, + array_agg(DISTINCT st.id) AS strains, array_agg(DISTINCT m.id) AS measurements + FROM strains st + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) + INNER JOIN measurements m ON m.strain_id=st.id + RIGHT OUTER JOIN characteristics c ON c.id=m.characteristic_id + INNER JOIN characteristic_types ct ON ct.id=c.characteristic_type_id` + vals = append(vals, opt.Genus) + + if len(opt.Ids) != 0 { + var counter int64 = 2 + w := helpers.ValsIn("c.id", opt.Ids, &vals, &counter) + + q += fmt.Sprintf(" WHERE %s", w) + } + + q += ` GROUP BY c.id, ct.characteristic_type_name + ORDER BY ct.characteristic_type_name, c.sort_order ASC;` + + var characteristics Characteristics + err := DBH.Select(&characteristics, q, vals...) + if err != nil { + return nil, err + } + + for _, c := range characteristics { + c.CanEdit = helpers.CanEdit(claims, c.CreatedBy) + } + + return &characteristics, nil +} + +func StrainOptsFromCharacteristics(opt helpers.ListOptions) (*helpers.ListOptions, error) { + relatedStrainIds := make([]int64, 0) + baseQ := `SELECT DISTINCT m.strain_id + FROM measurements m + INNER JOIN strains st ON st.id=m.strain_id + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1)` + if opt.Ids == nil { + q := fmt.Sprintf("%s;", baseQ) + if err := DBH.Select(&relatedStrainIds, q, opt.Genus); err != nil { + return nil, err + } + } else { + var vals []interface{} + var count int64 = 2 + vals = append(vals, opt.Genus) + q := fmt.Sprintf("%s WHERE %s ", baseQ, helpers.ValsIn("m.characteristic_id", opt.Ids, &vals, &count)) + + if err := DBH.Select(&relatedStrainIds, q, vals...); err != nil { + return nil, err + } + } + + return &helpers.ListOptions{Genus: opt.Genus, Ids: relatedStrainIds}, nil +} + +func MeasurementOptsFromCharacteristics(opt helpers.ListOptions) (*helpers.MeasurementListOptions, error) { + relatedMeasurementIds := make([]int64, 0) + baseQ := `SELECT m.id + FROM measurements m + INNER JOIN strains st ON st.id=m.strain_id + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1)` + + if opt.Ids == nil { + q := fmt.Sprintf("%s;", baseQ) + if err := DBH.Select(&relatedMeasurementIds, q, opt.Genus); err != nil { + return nil, err + } + } else { + var vals []interface{} + var count int64 = 2 + vals = append(vals, opt.Genus) + q := fmt.Sprintf("%s WHERE %s;", baseQ, helpers.ValsIn("characteristic_id", opt.Ids, &vals, &count)) + + if err := DBH.Select(&relatedMeasurementIds, q, vals...); err != nil { + return nil, err + } + } + + return &helpers.MeasurementListOptions{ListOptions: helpers.ListOptions{Genus: opt.Genus, Ids: relatedMeasurementIds}, Strains: nil, Characteristics: nil}, nil +} + +func StrainsFromCharacteristicId(id int64, genus string, claims *types.Claims) (*Strains, *helpers.ListOptions, error) { + opt := helpers.ListOptions{ + Genus: genus, + Ids: []int64{id}, + } + + strains_opt, err := StrainOptsFromCharacteristics(opt) + if err != nil { + return nil, nil, err + } + + strains, err := ListStrains(*strains_opt, claims) + if err != nil { + return nil, nil, err + } + + return strains, strains_opt, nil +} + +func MeasurementsFromCharacteristicId(id int64, genus string, claims *types.Claims) (*Measurements, *helpers.MeasurementListOptions, error) { + opt := helpers.ListOptions{ + Genus: genus, + Ids: []int64{id}, + } + + measurement_opt, err := MeasurementOptsFromCharacteristics(opt) + if err != nil { + return nil, nil, err + } + + measurements, err := ListMeasurements(*measurement_opt, claims) + if err != nil { + return nil, nil, err + } + + return measurements, measurement_opt, nil +} + +func GetCharacteristic(id int64, genus string, claims *types.Claims) (*Characteristic, error) { + var characteristic Characteristic + q := `SELECT c.*, ct.characteristic_type_name, + array_agg(DISTINCT st.id) AS strains, array_agg(DISTINCT m.id) AS measurements + FROM strains st + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) + INNER JOIN measurements m ON m.strain_id=st.id + RIGHT OUTER JOIN characteristics c ON c.id=m.characteristic_id + INNER JOIN characteristic_types ct ON ct.id=c.characteristic_type_id + WHERE c.id=$2 + GROUP BY c.id, ct.characteristic_type_name;` + if err := DBH.SelectOne(&characteristic, q, genus, id); err != nil { + if err == sql.ErrNoRows { + return nil, ErrCharacteristicNotFound + } + return nil, err + } + + characteristic.CanEdit = helpers.CanEdit(claims, characteristic.CreatedBy) + + return &characteristic, nil +} + +func InsertOrGetCharacteristicType(val string, claims *types.Claims) (int64, error) { + var id int64 + q := `SELECT id FROM characteristic_types WHERE characteristic_type_name=$1;` + if err := DBH.SelectOne(&id, q, val); err != nil { + if err == sql.ErrNoRows { + i := `INSERT INTO characteristic_types + (characteristic_type_name, created_at, updated_at, created_by, updated_by) + VALUES ($1, $2, $3, $4, $5) RETURNING id;` + ct := helpers.CurrentTime() + var result sql.Result + var insertErr error + stmt, err := DB.Db.Prepare(i) + if result, insertErr = stmt.Exec(val, ct, ct, claims.Sub, claims.Sub); insertErr != nil { + return 0, insertErr + } + id, err = result.LastInsertId() + if err != nil { + return 0, err + } + } else { + return 0, err + } + } + return id, nil +} diff --git a/models/database.go b/models/database.go new file mode 100644 index 0000000..d243468 --- /dev/null +++ b/models/database.go @@ -0,0 +1,8 @@ +package models + +import "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" + +var ( + DB = &modl.DbMap{Dialect: modl.PostgresDialect{}} + DBH modl.SqlExecutor = DB +) diff --git a/models/measurements.go b/models/measurements.go new file mode 100644 index 0000000..1bfa6dd --- /dev/null +++ b/models/measurements.go @@ -0,0 +1,234 @@ +package models + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/types" +) + +var ( + ErrMeasurementNotFound = errors.New("Measurement not found") + ErrMeasurementNotFoundJSON = types.NewJSONError(ErrMeasurementNotFound, http.StatusNotFound) +) + +func init() { + DB.AddTableWithName(MeasurementBase{}, "measurements").SetKeys(true, "Id") +} + +func (m *MeasurementBase) PreInsert(e modl.SqlExecutor) error { + ct := helpers.CurrentTime() + m.CreatedAt = ct + m.UpdatedAt = ct + return nil +} + +func (m *MeasurementBase) PreUpdate(e modl.SqlExecutor) error { + m.UpdatedAt = helpers.CurrentTime() + return nil +} + +// There are three types of supported measurements: fixed-text, free-text, +// & numerical. The table has a constraint that will allow at most one +// for a particular combination of strain & characteristic. +// MeasurementBase is what the DB expects to see for inserts/updates +type MeasurementBase struct { + Id int64 `json:"id,omitempty"` + StrainId int64 `db:"strain_id" json:"strain"` + CharacteristicId int64 `db:"characteristic_id" json:"characteristic"` + TextMeasurementTypeId types.NullInt64 `db:"text_measurement_type_id" json:"-"` + TxtValue types.NullString `db:"txt_value" json:"-"` + NumValue types.NullFloat64 `db:"num_value" json:"-"` + ConfidenceInterval types.NullFloat64 `db:"confidence_interval" json:"confidenceInterval"` + UnitTypeId types.NullInt64 `db:"unit_type_id" json:"-"` + Notes types.NullString `db:"notes" json:"notes"` + TestMethodId types.NullInt64 `db:"test_method_id" json:"-"` + CreatedAt types.NullTime `db:"created_at" json:"createdAt"` + UpdatedAt types.NullTime `db:"updated_at" json:"updatedAt"` + CreatedBy int64 `db:"created_by" json:"createdBy"` + UpdatedBy int64 `db:"updated_by" json:"updatedBy"` +} + +type Measurement struct { + *MeasurementBase + TextMeasurementType types.NullString `db:"text_measurement_type_name" json:"-"` + UnitType types.NullString `db:"unit_type_name" json:"unitType"` + TestMethod types.NullString `db:"test_method_name" json:"testMethod"` + CanEdit bool `db:"-" json:"canEdit"` +} + +type FakeMeasurement Measurement + +func (m *Measurement) MarshalJSON() ([]byte, error) { + fm := FakeMeasurement(*m) + return json.Marshal(struct { + *FakeMeasurement + Value string `json:"value"` + }{ + FakeMeasurement: &fm, + Value: m.Value(), + }) +} + +func (m *Measurement) UnmarshalJSON(b []byte) error { + var measurement struct { + FakeMeasurement + Value interface{} `json:"value"` + } + if err := json.Unmarshal(b, &measurement); err != nil { + return err + } + + switch v := measurement.Value.(type) { + case string: + // Test if actually a lookup + id, err := GetTextMeasurementTypeId(v) + if err != nil { + if err == sql.ErrNoRows { + measurement.TxtValue = types.NullString{sql.NullString{String: v, Valid: true}} + } else { + return err + } + } else { + measurement.TextMeasurementTypeId = types.NullInt64{sql.NullInt64{Int64: id, Valid: true}} + } + case int64: + measurement.NumValue = types.NullFloat64{sql.NullFloat64{Float64: float64(v), Valid: true}} + case float64: + measurement.NumValue = types.NullFloat64{sql.NullFloat64{Float64: v, Valid: true}} + } + + *m = Measurement(measurement.FakeMeasurement) + + return nil +} + +func (m *Measurement) Value() string { + if m.TextMeasurementType.Valid { + return m.TextMeasurementType.String + } + if m.TxtValue.Valid { + return m.TxtValue.String + } + if m.NumValue.Valid { + return fmt.Sprintf("%f", m.NumValue.Float64) + } + return "" +} + +type Measurements []*Measurement + +type MeasurementMeta struct { + CanAdd bool `json:"canAdd"` +} + +func ListMeasurements(opt helpers.MeasurementListOptions, claims *types.Claims) (*Measurements, error) { + var vals []interface{} + + q := `SELECT m.*, t.text_measurement_name AS text_measurement_type_name, + u.symbol AS unit_type_name, te.name AS test_method_name + FROM measurements m + INNER JOIN strains st ON st.id=m.strain_id + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=$1 + LEFT OUTER JOIN characteristics c ON c.id=m.characteristic_id + LEFT OUTER JOIN text_measurement_types t ON t.id=m.text_measurement_type_id + LEFT OUTER JOIN unit_types u ON u.id=m.unit_type_id + LEFT OUTER JOIN test_methods te ON te.id=m.test_method_id` + vals = append(vals, opt.Genus) + + strainIds := len(opt.Strains) != 0 + charIds := len(opt.Characteristics) != 0 + ids := len(opt.Ids) != 0 + + if strainIds || charIds || ids { + var paramsCounter int64 = 2 + q += "\nWHERE (" + + // Filter by strains + if strainIds { + q += helpers.ValsIn("st.id", opt.Strains, &vals, ¶msCounter) + } + + if strainIds && (charIds || ids) { + q += " AND " + } + + // Filter by characteristics + if charIds { + q += helpers.ValsIn("c.id", opt.Characteristics, &vals, ¶msCounter) + } + + if charIds && ids { + q += " AND " + } + + // Get specific records + if ids { + q += helpers.ValsIn("m.id", opt.Ids, &vals, ¶msCounter) + } + q += ")" + } + q += ";" + + measurements := make(Measurements, 0) + err := DBH.Select(&measurements, q, vals...) + if err != nil { + return nil, err + } + + for _, m := range measurements { + m.CanEdit = helpers.CanEdit(claims, m.CreatedBy) + } + + return &measurements, nil +} + +func GetMeasurement(id int64, genus string, claims *types.Claims) (*Measurement, error) { + var measurement Measurement + + q := `SELECT m.*, t.text_measurement_name AS text_measurement_type_name, + u.symbol AS unit_type_name, te.name AS test_method_name + FROM measurements m + INNER JOIN strains st ON st.id=m.strain_id + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) + LEFT OUTER JOIN characteristics c ON c.id=m.characteristic_id + LEFT OUTER JOIN text_measurement_types t ON t.id=m.text_measurement_type_id + LEFT OUTER JOIN unit_types u ON u.id=m.unit_type_id + LEFT OUTER JOIN test_methods te ON te.id=m.test_method_id + WHERE m.id=$2;` + if err := DBH.SelectOne(&measurement, q, genus, id); err != nil { + if err == sql.ErrNoRows { + return nil, ErrMeasurementNotFound + } + return nil, err + } + + measurement.CanEdit = helpers.CanEdit(claims, measurement.CreatedBy) + + return &measurement, nil +} + +func CharacteristicOptsFromMeasurements(opt helpers.MeasurementListOptions) (*helpers.ListOptions, error) { + return &helpers.ListOptions{Genus: opt.Genus, Ids: opt.Characteristics}, nil +} + +func StrainOptsFromMeasurements(opt helpers.MeasurementListOptions) (*helpers.ListOptions, error) { + return &helpers.ListOptions{Genus: opt.Genus, Ids: opt.Strains}, nil +} + +func GetTextMeasurementTypeId(val string) (int64, error) { + var id int64 + q := `SELECT id FROM text_measurement_types WHERE text_measurement_name=$1;` + + if err := DBH.SelectOne(&id, q, val); err != nil { + return 0, err + } + return id, nil +} diff --git a/models/species.go b/models/species.go new file mode 100644 index 0000000..e88faed --- /dev/null +++ b/models/species.go @@ -0,0 +1,174 @@ +package models + +import ( + "database/sql" + "errors" + "fmt" + "strings" + + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/types" +) + +var ( + ErrSpeciesNotFound = errors.New("Species not found") + ErrSpeciesNotUpdated = errors.New("Species not updated") +) + +func init() { + DB.AddTableWithName(SpeciesBase{}, "species").SetKeys(true, "Id") +} + +func (s *SpeciesBase) PreInsert(e modl.SqlExecutor) error { + ct := helpers.CurrentTime() + s.CreatedAt = ct + s.UpdatedAt = ct + return nil +} + +func (s *SpeciesBase) PreUpdate(e modl.SqlExecutor) error { + s.UpdatedAt = helpers.CurrentTime() + return nil +} + +type SpeciesBase struct { + Id int64 `db:"id" json:"id"` + GenusID int64 `db:"genus_id" json:"-"` + SubspeciesSpeciesID types.NullInt64 `db:"subspecies_species_id" json:"-"` + SpeciesName string `db:"species_name" json:"speciesName"` + TypeSpecies types.NullBool `db:"type_species" json:"typeSpecies"` + Etymology types.NullString `db:"etymology" json:"etymology"` + CreatedAt types.NullTime `db:"created_at" json:"createdAt"` + UpdatedAt types.NullTime `db:"updated_at" json:"updatedAt"` + DeletedAt types.NullTime `db:"deleted_at" json:"deletedAt"` + CreatedBy int64 `db:"created_by" json:"createdBy"` + UpdatedBy int64 `db:"updated_by" json:"updatedBy"` + DeletedBy types.NullInt64 `db:"deleted_by" json:"deletedBy"` +} + +type Species struct { + *SpeciesBase + GenusName string `db:"genus_name" json:"genusName"` + Strains types.NullSliceInt64 `db:"strains" json:"strains"` + TotalStrains int64 `db:"total_strains" json:"totalStrains"` + SortOrder int64 `db:"sort_order" json:"sortOrder"` + CanEdit bool `db:"-" json:"canEdit"` +} + +type ManySpecies []*Species + +type SpeciesMeta struct { + CanAdd bool `json:"canAdd"` +} + +func GenusIdFromName(genus_name string) (int64, error) { + var genus_id struct{ Id int64 } + q := `SELECT id FROM genera WHERE LOWER(genus_name) = LOWER($1);` + if err := DBH.SelectOne(&genus_id, q, genus_name); err != nil { + return 0, err + } + return genus_id.Id, nil +} + +func StrainOptsFromSpecies(opt helpers.ListOptions) (*helpers.ListOptions, error) { + relatedStrainIds := make([]int64, 0) + + if opt.Ids == nil { + q := `SELECT DISTINCT st.id + FROM strains st + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1);` + if err := DBH.Select(&relatedStrainIds, q, opt.Genus); err != nil { + return nil, err + } + } else { + var vals []interface{} + var count int64 = 1 + q := fmt.Sprintf("SELECT DISTINCT id FROM strains WHERE %s;", helpers.ValsIn("species_id", opt.Ids, &vals, &count)) + + if err := DBH.Select(&relatedStrainIds, q, vals...); err != nil { + return nil, err + } + } + + return &helpers.ListOptions{Genus: opt.Genus, Ids: relatedStrainIds}, nil +} + +func StrainsFromSpeciesId(id int64, genus string, claims *types.Claims) (*Strains, error) { + opt := helpers.ListOptions{ + Genus: genus, + Ids: []int64{id}, + } + + strains_opt, err := StrainOptsFromSpecies(opt) + if err != nil { + return nil, err + } + + strains, err := ListStrains(*strains_opt, claims) + if err != nil { + return nil, err + } + + return strains, nil +} + +func ListSpecies(opt helpers.ListOptions, claims *types.Claims) (*ManySpecies, error) { + var vals []interface{} + + q := `SELECT sp.*, g.genus_name, array_agg(st.id) AS strains, + COUNT(st) AS total_strains, + rank() OVER (ORDER BY sp.species_name ASC) AS sort_order + FROM species sp + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) + LEFT OUTER JOIN strains st ON st.species_id=sp.id` + vals = append(vals, opt.Genus) + + if len(opt.Ids) != 0 { + var conds []string + s := "sp.id IN (" + for i, id := range opt.Ids { + s = s + fmt.Sprintf("$%v,", i+2) // start param index at 2 + vals = append(vals, id) + } + s = s[:len(s)-1] + ")" + conds = append(conds, s) + q += " WHERE (" + strings.Join(conds, ") AND (") + ")" + } + + q += " GROUP BY sp.id, g.genus_name;" + + species := make(ManySpecies, 0) + err := DBH.Select(&species, q, vals...) + if err != nil { + return nil, err + } + + for _, s := range species { + s.CanEdit = helpers.CanEdit(claims, s.CreatedBy) + } + + return &species, nil +} + +func GetSpecies(id int64, genus string, claims *types.Claims) (*Species, error) { + var species Species + q := `SELECT sp.*, g.genus_name, array_agg(st.id) AS strains, + COUNT(st) AS total_strains, 0 AS sort_order + FROM species sp + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) + LEFT OUTER JOIN strains st ON st.species_id=sp.id + WHERE sp.id=$2 + GROUP BY sp.id, g.genus_name;` + if err := DBH.SelectOne(&species, q, genus, id); err != nil { + if err == sql.ErrNoRows { + return nil, ErrSpeciesNotFound + } + return nil, err + } + + species.CanEdit = helpers.CanEdit(claims, species.CreatedBy) + + return &species, nil +} diff --git a/models/strains.go b/models/strains.go new file mode 100644 index 0000000..a386031 --- /dev/null +++ b/models/strains.go @@ -0,0 +1,184 @@ +package models + +import ( + "database/sql" + "errors" + "fmt" + "strings" + + "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/types" +) + +var ( + ErrStrainNotFound = errors.New("Strain not found") + ErrStrainNotUpdated = errors.New("Strain not updated") +) + +func init() { + DB.AddTableWithName(StrainBase{}, "strains").SetKeys(true, "Id") +} + +func (s *StrainBase) PreInsert(e modl.SqlExecutor) error { + ct := helpers.CurrentTime() + s.CreatedAt = ct + s.UpdatedAt = ct + return nil +} + +func (s *StrainBase) PreUpdate(e modl.SqlExecutor) error { + s.UpdatedAt = helpers.CurrentTime() + return nil +} + +type StrainBase struct { + Id int64 `db:"id" json:"id"` + SpeciesId int64 `db:"species_id" json:"species"` + StrainName string `db:"strain_name" json:"strainName"` + TypeStrain bool `db:"type_strain" json:"typeStrain"` + AccessionNumbers types.NullString `db:"accession_numbers" json:"accessionNumbers"` + Genbank types.NullString `db:"genbank" json:"genbank"` + WholeGenomeSequence types.NullString `db:"whole_genome_sequence" json:"wholeGenomeSequence"` + IsolatedFrom types.NullString `db:"isolated_from" json:"isolatedFrom"` + Notes types.NullString `db:"notes" json:"notes"` + CreatedAt types.NullTime `db:"created_at" json:"createdAt"` + UpdatedAt types.NullTime `db:"updated_at" json:"updatedAt"` + DeletedAt types.NullTime `db:"deleted_at" json:"deletedAt"` + CreatedBy int64 `db:"created_by" json:"createdBy"` + UpdatedBy int64 `db:"updated_by" json:"updatedBy"` + DeletedBy types.NullInt64 `db:"deleted_by" json:"deletedBy"` +} + +type Strain struct { + *StrainBase + Measurements types.NullSliceInt64 `db:"measurements" json:"measurements"` + Characteristics types.NullSliceInt64 `db:"characteristics" json:"characteristics"` + TotalMeasurements int64 `db:"total_measurements" json:"totalMeasurements"` + SortOrder int64 `db:"sort_order" json:"sortOrder"` + CanEdit bool `db:"-" json:"canEdit"` +} + +type Strains []*Strain + +type StrainMeta struct { + CanAdd bool `json:"canAdd"` +} + +func (s StrainBase) SpeciesName() string { + var species SpeciesBase + if err := DBH.Get(&species, s.SpeciesId); err != nil { + return "" + } + return species.SpeciesName +} + +func ListStrains(opt helpers.ListOptions, claims *types.Claims) (*Strains, error) { + var vals []interface{} + + q := `SELECT st.*, array_agg(m.id) AS measurements, + array_agg(DISTINCT m.characteristic_id) AS characteristics, + COUNT(m) AS total_measurements, + rank() OVER (ORDER BY sp.species_name ASC, st.type_strain ASC, st.strain_name ASC) AS sort_order + FROM strains st + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) + LEFT OUTER JOIN measurements m ON m.strain_id=st.id` + vals = append(vals, opt.Genus) + + if len(opt.Ids) != 0 { + var conds []string + s := "st.id IN (" + for i, id := range opt.Ids { + s = s + fmt.Sprintf("$%v,", i+2) // start param index at 2 + vals = append(vals, id) + } + s = s[:len(s)-1] + ")" + conds = append(conds, s) + q += " WHERE (" + strings.Join(conds, ") AND (") + ")" + } + + q += " GROUP BY st.id, st.species_id, sp.species_name;" + + strains := make(Strains, 0) + err := DBH.Select(&strains, q, vals...) + if err != nil { + return nil, err + } + + for _, s := range strains { + s.CanEdit = helpers.CanEdit(claims, s.CreatedBy) + } + + return &strains, nil +} + +func GetStrain(id int64, genus string, claims *types.Claims) (*Strain, error) { + var strain Strain + q := `SELECT st.*, array_agg(DISTINCT m.id) AS measurements, + array_agg(DISTINCT m.characteristic_id) AS characteristics, + COUNT(m) AS total_measurements, 0 AS sort_order + FROM strains st + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) + LEFT OUTER JOIN measurements m ON m.strain_id=st.id + WHERE st.id=$2 + GROUP BY st.id;` + if err := DBH.SelectOne(&strain, q, genus, id); err != nil { + if err == sql.ErrNoRows { + return nil, ErrStrainNotFound + } + return nil, err + } + + strain.CanEdit = helpers.CanEdit(claims, strain.CreatedBy) + + return &strain, nil +} + +func SpeciesOptsFromStrains(opt helpers.ListOptions) (*helpers.ListOptions, error) { + relatedSpeciesIds := make([]int64, 0) + + if opt.Ids == nil || len(opt.Ids) == 0 { + q := `SELECT DISTINCT st.species_id + FROM strains st + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1);` + if err := DBH.Select(&relatedSpeciesIds, q, opt.Genus); err != nil { + return nil, err + } + } else { + var vals []interface{} + var count int64 = 1 + q := fmt.Sprintf("SELECT DISTINCT species_id FROM strains WHERE %s;", helpers.ValsIn("id", opt.Ids, &vals, &count)) + if err := DBH.Select(&relatedSpeciesIds, q, vals...); err != nil { + return nil, err + } + } + + return &helpers.ListOptions{Genus: opt.Genus, Ids: relatedSpeciesIds}, nil +} + +func CharacteristicsOptsFromStrains(opt helpers.ListOptions) (*helpers.ListOptions, error) { + relatedCharacteristicsIds := make([]int64, 0) + + if opt.Ids == nil || len(opt.Ids) == 0 { + q := `SELECT DISTINCT m.characteristic_id + FROM measurements m + INNER JOIN strains st ON st.id=m.strain_id + INNER JOIN species sp ON sp.id=st.species_id + INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1);` + if err := DBH.Select(&relatedCharacteristicsIds, q, opt.Genus); err != nil { + return nil, err + } + } else { + var vals []interface{} + var count int64 = 1 + q := fmt.Sprintf("SELECT DISTINCT characteristic_id FROM measurements WHERE %s;", helpers.ValsIn("strain_id", opt.Ids, &vals, &count)) + if err := DBH.Select(&relatedCharacteristicsIds, q, vals...); err != nil { + return nil, err + } + } + + return &helpers.ListOptions{Genus: opt.Genus, Ids: relatedCharacteristicsIds}, nil +} diff --git a/models/users.go b/models/users.go new file mode 100644 index 0000000..1cf56a8 --- /dev/null +++ b/models/users.go @@ -0,0 +1,156 @@ +package models + +import ( + "database/sql" + "encoding/json" + "errors" + "regexp" + + "github.com/thermokarst/bactdb/Godeps/_workspace/src/golang.org/x/crypto/bcrypt" + "github.com/thermokarst/bactdb/helpers" + "github.com/thermokarst/bactdb/types" +) + +var ( + ErrUserNotFound = errors.New("User not found") + ErrUserNotUpdated = errors.New("User not updated") + ErrInvalidEmailOrPassword = errors.New("Invalid email or password") + ErrEmailAddressTaken = errors.New("Email address already registered") +) + +func init() { + DB.AddTableWithName(UserBase{}, "users").SetKeys(true, "Id") +} + +type UserBase struct { + Id int64 `json:"id,omitempty"` + Email string `db:"email" json:"email"` + Password string `db:"password" json:"password,omitempty"` + Name string `db:"name" json:"name"` + Role string `db:"role" json:"role"` + Verified bool `db:"verified" json:"-"` + CreatedAt types.NullTime `db:"created_at" json:"createdAt"` + UpdatedAt types.NullTime `db:"updated_at" json:"updatedAt"` + DeletedAt types.NullTime `db:"deleted_at" json:"deletedAt"` +} + +type User struct { + *UserBase + CanEdit bool `db:"-" json:"canEdit"` +} + +type UserValidation struct { + Email []string `json:"email,omitempty"` + Password []string `json:"password,omitempty"` + Name []string `json:"name,omitempty"` + Role []string `json:"role,omitempty"` +} + +func (uv UserValidation) Error() string { + errs, err := json.Marshal(struct { + UserValidation `json:"errors"` + }{uv}) + if err != nil { + return err.Error() + } + return string(errs) +} + +type Users []*User + +type UserJSON struct { + User *User `json:"user"` +} + +type UsersJSON struct { + Users *Users `json:"users"` +} + +type UserMeta struct { + CanAdd bool `json:"canAdd"` +} + +func (u *Users) Marshal() ([]byte, error) { + return json.Marshal(&UsersJSON{Users: u}) +} + +func (u *User) Validate() error { + var uv UserValidation + validationError := false + + if u.Name == "" { + uv.Name = append(uv.Name, helpers.MustProvideAValue) + validationError = true + } + + if u.Email == "" { + uv.Email = append(uv.Email, helpers.MustProvideAValue) + validationError = true + } + + regex, _ := regexp.Compile(`(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})`) + if u.Email != "" && !regex.MatchString(u.Email) { + uv.Email = append(uv.Email, "Must provide a valid email address") + validationError = true + } + + if len(u.Password) < 8 { + uv.Password = append(uv.Password, "Password must be at least 8 characters") + validationError = true + } + + if validationError { + return uv + } + return nil +} + +// for thermokarst/jwt: authentication callback +func DbAuthenticate(email string, password string) error { + var user User + q := `SELECT * + FROM users + WHERE lower(email)=lower($1) + AND verified IS TRUE + AND deleted_at IS NULL;` + if err := DBH.SelectOne(&user, q, email); err != nil { + return ErrInvalidEmailOrPassword + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return ErrInvalidEmailOrPassword + } + return nil +} + +func DbGetUserById(id int64) (*User, error) { + var user User + q := `SELECT * + FROM users + WHERE id=$1 + AND verified IS TRUE + AND deleted_at IS NULL;` + if err := DBH.SelectOne(&user, q, id); err != nil { + if err == sql.ErrNoRows { + return nil, ErrUserNotFound + } + return nil, err + } + return &user, nil +} + +// for thermokarst/jwt: setting user in claims bundle +func DbGetUserByEmail(email string) (*User, error) { + var user User + q := `SELECT * + FROM users + WHERE lower(email)=lower($1) + AND verified IS TRUE + AND deleted_at IS NULL;` + if err := DBH.SelectOne(&user, q, email); err != nil { + if err == sql.ErrNoRows { + return nil, ErrUserNotFound + } + return nil, err + } + return &user, nil +} diff --git a/payloads/payloads.go b/payloads/payloads.go new file mode 100644 index 0000000..cabdd1c --- /dev/null +++ b/payloads/payloads.go @@ -0,0 +1,102 @@ +package payloads + +import ( + "encoding/json" + + "github.com/thermokarst/bactdb/models" +) + +type CharacteristicPayload struct { + Characteristic *models.Characteristic `json:"characteristic"` + Measurements *models.Measurements `json:"measurements"` + Strains *models.Strains `json:"strains"` + Species *models.ManySpecies `json:"species"` + Meta *models.CharacteristicMeta `json:"meta"` +} + +type CharacteristicsPayload struct { + Characteristics *models.Characteristics `json:"characteristics"` + Measurements *models.Measurements `json:"measurements"` + Strains *models.Strains `json:"strains"` + Species *models.ManySpecies `json:"species"` + Meta *models.CharacteristicMeta `json:"meta"` +} + +func (c *CharacteristicPayload) Marshal() ([]byte, error) { + return json.Marshal(c) +} + +func (c *CharacteristicsPayload) Marshal() ([]byte, error) { + return json.Marshal(c) +} + +type MeasurementPayload struct { + Measurement *models.Measurement `json:"measurement"` +} + +type MeasurementsPayload struct { + Strains *models.Strains `json:"strains"` + Characteristics *models.Characteristics `json:"characteristics"` + Measurements *models.Measurements `json:"measurements"` +} + +func (m *MeasurementPayload) Marshal() ([]byte, error) { + return json.Marshal(m) +} + +func (m *MeasurementsPayload) Marshal() ([]byte, error) { + return json.Marshal(m) +} + +type SpeciesPayload struct { + Species *models.Species `json:"species"` + Strains *models.Strains `json:"strains"` + Meta *models.SpeciesMeta `json:"meta"` +} + +type ManySpeciesPayload struct { + Species *models.ManySpecies `json:"species"` + Strains *models.Strains `json:"strains"` + Meta *models.SpeciesMeta `json:"meta"` +} + +func (s *SpeciesPayload) Marshal() ([]byte, error) { + return json.Marshal(s) +} + +func (s *ManySpeciesPayload) Marshal() ([]byte, error) { + return json.Marshal(s) +} + +type StrainPayload struct { + Strain *models.Strain `json:"strain"` + Species *models.ManySpecies `json:"species"` + Characteristics *models.Characteristics `json:"characteristics"` + Measurements *models.Measurements `json:"measurements"` + Meta *models.StrainMeta `json:"meta"` +} + +type StrainsPayload struct { + Strains *models.Strains `json:"strains"` + Species *models.ManySpecies `json:"species"` + Characteristics *models.Characteristics `json:"characteristics"` + Measurements *models.Measurements `json:"measurements"` + Meta *models.StrainMeta `json:"meta"` +} + +func (s *StrainPayload) Marshal() ([]byte, error) { + return json.Marshal(s) +} + +func (s *StrainsPayload) Marshal() ([]byte, error) { + return json.Marshal(s) +} + +type UserPayload struct { + User *models.User `json:"user"` + Meta *models.UserMeta `json:"meta"` +} + +func (u *UserPayload) Marshal() ([]byte, error) { + return json.Marshal(u) +} diff --git a/species.go b/species.go deleted file mode 100644 index 3aa0f45..0000000 --- a/species.go +++ /dev/null @@ -1,330 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" -) - -var ( - ErrSpeciesNotFound = errors.New("Species not found") - ErrSpeciesNotUpdated = errors.New("Species not updated") -) - -func init() { - DB.AddTableWithName(SpeciesBase{}, "species").SetKeys(true, "Id") -} - -func (s *SpeciesBase) PreInsert(e modl.SqlExecutor) error { - ct := currentTime() - s.CreatedAt = ct - s.UpdatedAt = ct - return nil -} - -func (s *SpeciesBase) PreUpdate(e modl.SqlExecutor) error { - s.UpdatedAt = currentTime() - return nil -} - -type SpeciesService struct{} - -type SpeciesBase struct { - Id int64 `db:"id" json:"id"` - GenusID int64 `db:"genus_id" json:"-"` - SubspeciesSpeciesID NullInt64 `db:"subspecies_species_id" json:"-"` - SpeciesName string `db:"species_name" json:"speciesName"` - TypeSpecies NullBool `db:"type_species" json:"typeSpecies"` - Etymology NullString `db:"etymology" json:"etymology"` - CreatedAt NullTime `db:"created_at" json:"createdAt"` - UpdatedAt NullTime `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` - CreatedBy int64 `db:"created_by" json:"createdBy"` - UpdatedBy int64 `db:"updated_by" json:"updatedBy"` - DeletedBy NullInt64 `db:"deleted_by" json:"deletedBy"` -} - -type Species struct { - *SpeciesBase - GenusName string `db:"genus_name" json:"genusName"` - Strains NullSliceInt64 `db:"strains" json:"strains"` - TotalStrains int64 `db:"total_strains" json:"totalStrains"` - SortOrder int64 `db:"sort_order" json:"sortOrder"` - CanEdit bool `db:"-" json:"canEdit"` -} - -type ManySpecies []*Species - -type SpeciesMeta struct { - CanAdd bool `json:"canAdd"` -} - -type SpeciesPayload struct { - Species *Species `json:"species"` - Strains *Strains `json:"strains"` - Meta *SpeciesMeta `json:"meta"` -} - -type ManySpeciesPayload struct { - Species *ManySpecies `json:"species"` - Strains *Strains `json:"strains"` - Meta *SpeciesMeta `json:"meta"` -} - -func (s *SpeciesPayload) marshal() ([]byte, error) { - return json.Marshal(s) -} - -func (s *ManySpeciesPayload) marshal() ([]byte, error) { - return json.Marshal(s) -} - -func (s SpeciesService) unmarshal(b []byte) (entity, error) { - var sj SpeciesPayload - err := json.Unmarshal(b, &sj) - return &sj, err -} - -func (s SpeciesService) list(val *url.Values, claims *Claims) (entity, *appError) { - if val == nil { - return nil, ErrMustProvideOptionsJSON - } - var opt ListOptions - if err := schemaDecoder.Decode(&opt, *val); err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species, err := listSpecies(opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains_opt, err := strainOptsFromSpecies(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains, err := listStrains(*strains_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - payload := ManySpeciesPayload{ - Species: species, - Strains: strains, - Meta: &SpeciesMeta{ - CanAdd: canAdd(claims), - }, - } - - return &payload, nil -} - -func (s SpeciesService) get(id int64, genus string, claims *Claims) (entity, *appError) { - species, err := getSpecies(id, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains, err := strainsFromSpeciesId(id, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - payload := SpeciesPayload{ - Species: species, - Strains: strains, - Meta: &SpeciesMeta{ - CanAdd: canAdd(claims), - }, - } - - return &payload, nil -} - -func (s SpeciesService) update(id int64, e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*SpeciesPayload) - payload.Species.UpdatedBy = claims.Sub - payload.Species.Id = id - - genus_id, err := genusIdFromName(genus) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - payload.Species.SpeciesBase.GenusID = genus_id - - count, err := DBH.Update(payload.Species.SpeciesBase) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - if count != 1 { - return newJSONError(ErrSpeciesNotUpdated, http.StatusBadRequest) - } - - // Reload to send back down the wire - species, err := getSpecies(id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - strains, err := strainsFromSpeciesId(id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - payload.Species = species - payload.Strains = strains - payload.Meta = &SpeciesMeta{ - CanAdd: canAdd(claims), - } - - return nil -} - -func (s SpeciesService) create(e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*SpeciesPayload) - payload.Species.CreatedBy = claims.Sub - payload.Species.UpdatedBy = claims.Sub - - genus_id, err := genusIdFromName(genus) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - payload.Species.SpeciesBase.GenusID = genus_id - - err = DBH.Insert(payload.Species.SpeciesBase) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - // Reload to send back down the wire - species, err := getSpecies(payload.Species.Id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - // Note, no strains when new species - - payload.Species = species - payload.Meta = &SpeciesMeta{ - CanAdd: canAdd(claims), - } - return nil -} - -func genusIdFromName(genus_name string) (int64, error) { - var genus_id struct{ Id int64 } - q := `SELECT id FROM genera WHERE LOWER(genus_name) = LOWER($1);` - if err := DBH.SelectOne(&genus_id, q, genus_name); err != nil { - return 0, err - } - return genus_id.Id, nil -} - -func strainOptsFromSpecies(opt ListOptions) (*ListOptions, error) { - relatedStrainIds := make([]int64, 0) - - if opt.Ids == nil { - q := `SELECT DISTINCT st.id - FROM strains st - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1);` - if err := DBH.Select(&relatedStrainIds, q, opt.Genus); err != nil { - return nil, err - } - } else { - var vals []interface{} - var count int64 = 1 - q := fmt.Sprintf("SELECT DISTINCT id FROM strains WHERE %s;", valsIn("species_id", opt.Ids, &vals, &count)) - - if err := DBH.Select(&relatedStrainIds, q, vals...); err != nil { - return nil, err - } - } - - return &ListOptions{Genus: opt.Genus, Ids: relatedStrainIds}, nil -} - -func strainsFromSpeciesId(id int64, genus string, claims *Claims) (*Strains, error) { - opt := ListOptions{ - Genus: genus, - Ids: []int64{id}, - } - - strains_opt, err := strainOptsFromSpecies(opt) - if err != nil { - return nil, err - } - - strains, err := listStrains(*strains_opt, claims) - if err != nil { - return nil, err - } - - return strains, nil -} - -func listSpecies(opt ListOptions, claims *Claims) (*ManySpecies, error) { - var vals []interface{} - - q := `SELECT sp.*, g.genus_name, array_agg(st.id) AS strains, - COUNT(st) AS total_strains, - rank() OVER (ORDER BY sp.species_name ASC) AS sort_order - FROM species sp - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) - LEFT OUTER JOIN strains st ON st.species_id=sp.id` - vals = append(vals, opt.Genus) - - if len(opt.Ids) != 0 { - var conds []string - s := "sp.id IN (" - for i, id := range opt.Ids { - s = s + fmt.Sprintf("$%v,", i+2) // start param index at 2 - vals = append(vals, id) - } - s = s[:len(s)-1] + ")" - conds = append(conds, s) - q += " WHERE (" + strings.Join(conds, ") AND (") + ")" - } - - q += " GROUP BY sp.id, g.genus_name;" - - species := make(ManySpecies, 0) - err := DBH.Select(&species, q, vals...) - if err != nil { - return nil, err - } - - for _, s := range species { - s.CanEdit = canEdit(claims, s.CreatedBy) - } - - return &species, nil -} - -func getSpecies(id int64, genus string, claims *Claims) (*Species, error) { - var species Species - q := `SELECT sp.*, g.genus_name, array_agg(st.id) AS strains, - COUNT(st) AS total_strains, 0 AS sort_order - FROM species sp - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) - LEFT OUTER JOIN strains st ON st.species_id=sp.id - WHERE sp.id=$2 - GROUP BY sp.id, g.genus_name;` - if err := DBH.SelectOne(&species, q, genus, id); err != nil { - if err == sql.ErrNoRows { - return nil, ErrSpeciesNotFound - } - return nil, err - } - - species.CanEdit = canEdit(claims, species.CreatedBy) - - return &species, nil -} diff --git a/strains.go b/strains.go deleted file mode 100644 index 7851e7d..0000000 --- a/strains.go +++ /dev/null @@ -1,406 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl" -) - -var ( - ErrStrainNotFound = errors.New("Strain not found") - ErrStrainNotUpdated = errors.New("Strain not updated") -) - -func init() { - DB.AddTableWithName(StrainBase{}, "strains").SetKeys(true, "Id") -} - -func (s *StrainBase) PreInsert(e modl.SqlExecutor) error { - ct := currentTime() - s.CreatedAt = ct - s.UpdatedAt = ct - return nil -} - -func (s *StrainBase) PreUpdate(e modl.SqlExecutor) error { - s.UpdatedAt = currentTime() - return nil -} - -type StrainService struct{} - -type StrainBase struct { - Id int64 `db:"id" json:"id"` - SpeciesId int64 `db:"species_id" json:"species"` - StrainName string `db:"strain_name" json:"strainName"` - TypeStrain bool `db:"type_strain" json:"typeStrain"` - AccessionNumbers NullString `db:"accession_numbers" json:"accessionNumbers"` - Genbank NullString `db:"genbank" json:"genbank"` - WholeGenomeSequence NullString `db:"whole_genome_sequence" json:"wholeGenomeSequence"` - IsolatedFrom NullString `db:"isolated_from" json:"isolatedFrom"` - Notes NullString `db:"notes" json:"notes"` - CreatedAt NullTime `db:"created_at" json:"createdAt"` - UpdatedAt NullTime `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` - CreatedBy int64 `db:"created_by" json:"createdBy"` - UpdatedBy int64 `db:"updated_by" json:"updatedBy"` - DeletedBy NullInt64 `db:"deleted_by" json:"deletedBy"` -} - -type Strain struct { - *StrainBase - Measurements NullSliceInt64 `db:"measurements" json:"measurements"` - Characteristics NullSliceInt64 `db:"characteristics" json:"characteristics"` - TotalMeasurements int64 `db:"total_measurements" json:"totalMeasurements"` - SortOrder int64 `db:"sort_order" json:"sortOrder"` - CanEdit bool `db:"-" json:"canEdit"` -} - -type Strains []*Strain - -type StrainMeta struct { - CanAdd bool `json:"canAdd"` -} - -type StrainPayload struct { - Strain *Strain `json:"strain"` - Species *ManySpecies `json:"species"` - Characteristics *Characteristics `json:"characteristics"` - Measurements *Measurements `json:"measurements"` - Meta *StrainMeta `json:"meta"` -} - -type StrainsPayload struct { - Strains *Strains `json:"strains"` - Species *ManySpecies `json:"species"` - Characteristics *Characteristics `json:"characteristics"` - Measurements *Measurements `json:"measurements"` - Meta *StrainMeta `json:"meta"` -} - -func (s *StrainPayload) marshal() ([]byte, error) { - return json.Marshal(s) -} - -func (s *StrainsPayload) marshal() ([]byte, error) { - return json.Marshal(s) -} - -func (s StrainService) unmarshal(b []byte) (entity, error) { - var sj StrainPayload - err := json.Unmarshal(b, &sj) - return &sj, err -} - -func (s StrainBase) SpeciesName() string { - var species SpeciesBase - if err := DBH.Get(&species, s.SpeciesId); err != nil { - return "" - } - return species.SpeciesName -} - -func (s StrainService) list(val *url.Values, claims *Claims) (entity, *appError) { - if val == nil { - return nil, ErrMustProvideOptionsJSON - } - var opt ListOptions - if err := schemaDecoder.Decode(&opt, *val); err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - strains, err := listStrains(opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species_opt, err := speciesOptsFromStrains(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species, err := listSpecies(*species_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - characteristics_opt, err := characteristicsOptsFromStrains(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - characteristics, err := listCharacteristics(*characteristics_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - characteristic_ids := []int64{} - for _, c := range *characteristics { - characteristic_ids = append(characteristic_ids, c.Id) - } - - strain_ids := []int64{} - for _, s := range *strains { - strain_ids = append(strain_ids, s.Id) - } - - measurement_opt := MeasurementListOptions{ - ListOptions: ListOptions{ - Genus: opt.Genus, - }, - Strains: strain_ids, - Characteristics: characteristic_ids, - } - - measurements, err := listMeasurements(measurement_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - payload := StrainsPayload{ - Strains: strains, - Species: species, - Measurements: measurements, - Characteristics: characteristics, - Meta: &StrainMeta{ - CanAdd: canAdd(claims), - }, - } - - return &payload, nil -} - -func (s StrainService) get(id int64, genus string, claims *Claims) (entity, *appError) { - strain, err := getStrain(id, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - species, err := getSpecies(strain.SpeciesId, genus, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - opt := ListOptions{Genus: genus, Ids: []int64{id}} - characteristics_opt, err := characteristicsOptsFromStrains(opt) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - characteristics, err := listCharacteristics(*characteristics_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - characteristic_ids := []int64{} - for _, c := range *characteristics { - characteristic_ids = append(characteristic_ids, c.Id) - } - - measurement_opt := MeasurementListOptions{ - ListOptions: ListOptions{ - Genus: genus, - }, - Strains: []int64{id}, - Characteristics: characteristic_ids, - } - - measurements, err := listMeasurements(measurement_opt, claims) - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - var many_species ManySpecies = []*Species{species} - - payload := StrainPayload{ - Strain: strain, - Species: &many_species, - Characteristics: characteristics, - Measurements: measurements, - Meta: &StrainMeta{ - CanAdd: canAdd(claims), - }, - } - - return &payload, nil -} - -func (s StrainService) update(id int64, e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*StrainPayload) - payload.Strain.UpdatedBy = claims.Sub - payload.Strain.Id = id - - count, err := DBH.Update(payload.Strain.StrainBase) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - if count != 1 { - return newJSONError(ErrStrainNotUpdated, http.StatusBadRequest) - } - - strain, err := getStrain(id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - species, err := getSpecies(strain.SpeciesId, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - var many_species ManySpecies = []*Species{species} - - payload.Strain = strain - payload.Species = &many_species - payload.Meta = &StrainMeta{ - CanAdd: canAdd(claims), - } - - return nil -} - -func (s StrainService) create(e *entity, genus string, claims *Claims) *appError { - payload := (*e).(*StrainPayload) - payload.Strain.CreatedBy = claims.Sub - payload.Strain.UpdatedBy = claims.Sub - - if err := DBH.Insert(payload.Strain.StrainBase); err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - strain, err := getStrain(payload.Strain.Id, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - species, err := getSpecies(strain.SpeciesId, genus, claims) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - var many_species ManySpecies = []*Species{species} - - payload.Strain = strain - payload.Species = &many_species - payload.Meta = &StrainMeta{ - CanAdd: canAdd(claims), - } - - return nil -} - -func listStrains(opt ListOptions, claims *Claims) (*Strains, error) { - var vals []interface{} - - q := `SELECT st.*, array_agg(m.id) AS measurements, - array_agg(DISTINCT m.characteristic_id) AS characteristics, - COUNT(m) AS total_measurements, - rank() OVER (ORDER BY sp.species_name ASC, st.type_strain ASC, st.strain_name ASC) AS sort_order - FROM strains st - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) - LEFT OUTER JOIN measurements m ON m.strain_id=st.id` - vals = append(vals, opt.Genus) - - if len(opt.Ids) != 0 { - var conds []string - s := "st.id IN (" - for i, id := range opt.Ids { - s = s + fmt.Sprintf("$%v,", i+2) // start param index at 2 - vals = append(vals, id) - } - s = s[:len(s)-1] + ")" - conds = append(conds, s) - q += " WHERE (" + strings.Join(conds, ") AND (") + ")" - } - - q += " GROUP BY st.id, st.species_id, sp.species_name;" - - strains := make(Strains, 0) - err := DBH.Select(&strains, q, vals...) - if err != nil { - return nil, err - } - - for _, s := range strains { - s.CanEdit = canEdit(claims, s.CreatedBy) - } - - return &strains, nil -} - -func getStrain(id int64, genus string, claims *Claims) (*Strain, error) { - var strain Strain - q := `SELECT st.*, array_agg(DISTINCT m.id) AS measurements, - array_agg(DISTINCT m.characteristic_id) AS characteristics, - COUNT(m) AS total_measurements, 0 AS sort_order - FROM strains st - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1) - LEFT OUTER JOIN measurements m ON m.strain_id=st.id - WHERE st.id=$2 - GROUP BY st.id;` - if err := DBH.SelectOne(&strain, q, genus, id); err != nil { - if err == sql.ErrNoRows { - return nil, ErrStrainNotFound - } - return nil, err - } - - strain.CanEdit = canEdit(claims, strain.CreatedBy) - - return &strain, nil -} - -func speciesOptsFromStrains(opt ListOptions) (*ListOptions, error) { - relatedSpeciesIds := make([]int64, 0) - - if opt.Ids == nil || len(opt.Ids) == 0 { - q := `SELECT DISTINCT st.species_id - FROM strains st - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1);` - if err := DBH.Select(&relatedSpeciesIds, q, opt.Genus); err != nil { - return nil, err - } - } else { - var vals []interface{} - var count int64 = 1 - q := fmt.Sprintf("SELECT DISTINCT species_id FROM strains WHERE %s;", valsIn("id", opt.Ids, &vals, &count)) - if err := DBH.Select(&relatedSpeciesIds, q, vals...); err != nil { - return nil, err - } - } - - return &ListOptions{Genus: opt.Genus, Ids: relatedSpeciesIds}, nil -} - -func characteristicsOptsFromStrains(opt ListOptions) (*ListOptions, error) { - relatedCharacteristicsIds := make([]int64, 0) - - if opt.Ids == nil || len(opt.Ids) == 0 { - q := `SELECT DISTINCT m.characteristic_id - FROM measurements m - INNER JOIN strains st ON st.id=m.strain_id - INNER JOIN species sp ON sp.id=st.species_id - INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=LOWER($1);` - if err := DBH.Select(&relatedCharacteristicsIds, q, opt.Genus); err != nil { - return nil, err - } - } else { - var vals []interface{} - var count int64 = 1 - q := fmt.Sprintf("SELECT DISTINCT characteristic_id FROM measurements WHERE %s;", valsIn("strain_id", opt.Ids, &vals, &count)) - if err := DBH.Select(&relatedCharacteristicsIds, q, vals...); err != nil { - return nil, err - } - } - - return &ListOptions{Genus: opt.Genus, Ids: relatedCharacteristicsIds}, nil -} diff --git a/types/claims.go b/types/claims.go new file mode 100644 index 0000000..86c8a74 --- /dev/null +++ b/types/claims.go @@ -0,0 +1,11 @@ +package types + +type Claims struct { + Name string + Iss string + Sub int64 + Role string + Iat int64 + Exp int64 + Ref string +} diff --git a/types/entities.go b/types/entities.go new file mode 100644 index 0000000..5e99b99 --- /dev/null +++ b/types/entities.go @@ -0,0 +1,5 @@ +package types + +type Entity interface { + Marshal() ([]byte, error) +} diff --git a/types.go b/types/types.go similarity index 97% rename from types.go rename to types/types.go index 7e022eb..ab2a417 100644 --- a/types.go +++ b/types/types.go @@ -1,4 +1,4 @@ -package main +package types import ( "bytes" @@ -198,13 +198,13 @@ func (ej ErrorJSON) Error() string { return string(e) } -type appError struct { +type AppError struct { Error error Status int } -func newJSONError(err error, status int) *appError { - return &appError{ +func NewJSONError(err error, status int) *AppError { + return &AppError{ Error: ErrorJSON{Err: err}, Status: status, } diff --git a/users.go b/users.go deleted file mode 100644 index db93a6a..0000000 --- a/users.go +++ /dev/null @@ -1,392 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "log" - "net/http" - "net/url" - "regexp" - - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/gorilla/mux" - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/lib/pq" - "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/mailgun/mailgun-go" - "github.com/thermokarst/bactdb/Godeps/_workspace/src/golang.org/x/crypto/bcrypt" -) - -var ( - ErrUserNotFound = errors.New("User not found") - ErrUserNotFoundJSON = newJSONError(ErrUserNotFound, http.StatusNotFound) - ErrUserNotUpdated = errors.New("User not updated") - ErrUserNotUpdatedJSON = newJSONError(ErrUserNotUpdated, http.StatusBadRequest) - ErrInvalidEmailOrPassword = errors.New("Invalid email or password") - ErrEmailAddressTaken = errors.New("Email address already registered") - ErrEmailAddressTakenJSON = newJSONError(ErrEmailAddressTaken, http.StatusBadRequest) -) - -func init() { - DB.AddTableWithName(UserBase{}, "users").SetKeys(true, "Id") -} - -type UserService struct{} - -type UserBase struct { - Id int64 `json:"id,omitempty"` - Email string `db:"email" json:"email"` - Password string `db:"password" json:"password,omitempty"` - Name string `db:"name" json:"name"` - Role string `db:"role" json:"role"` - Verified bool `db:"verified" json:"-"` - CreatedAt NullTime `db:"created_at" json:"createdAt"` - UpdatedAt NullTime `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -type User struct { - *UserBase - CanEdit bool `db:"-" json:"canEdit"` -} - -type UserValidation struct { - Email []string `json:"email,omitempty"` - Password []string `json:"password,omitempty"` - Name []string `json:"name,omitempty"` - Role []string `json:"role,omitempty"` -} - -func (uv UserValidation) Error() string { - errs, err := json.Marshal(struct { - UserValidation `json:"errors"` - }{uv}) - if err != nil { - return err.Error() - } - return string(errs) -} - -type Users []*User - -type UserJSON struct { - User *User `json:"user"` -} - -type UsersJSON struct { - Users *Users `json:"users"` -} - -type UserMeta struct { - CanAdd bool `json:"canAdd"` -} - -type UserPayload struct { - User *User `json:"user"` - Meta *UserMeta `json:"meta"` -} - -func (u *UserPayload) marshal() ([]byte, error) { - return json.Marshal(u) -} - -func (u *Users) marshal() ([]byte, error) { - return json.Marshal(&UsersJSON{Users: u}) -} - -func (u UserService) unmarshal(b []byte) (entity, error) { - var uj UserPayload - err := json.Unmarshal(b, &uj) - return &uj, err -} - -func (u *User) validate() error { - var uv UserValidation - validationError := false - - if u.Name == "" { - uv.Name = append(uv.Name, MustProvideAValue) - validationError = true - } - - if u.Email == "" { - uv.Email = append(uv.Email, MustProvideAValue) - validationError = true - } - - regex, _ := regexp.Compile(`(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})`) - if u.Email != "" && !regex.MatchString(u.Email) { - uv.Email = append(uv.Email, "Must provide a valid email address") - validationError = true - } - - if len(u.Password) < 8 { - uv.Password = append(uv.Password, "Password must be at least 8 characters") - validationError = true - } - - if validationError { - return uv - } - return nil -} - -func (u UserService) list(val *url.Values, claims *Claims) (entity, *appError) { - if val == nil { - return nil, ErrMustProvideOptionsJSON - } - var opt ListOptions - if err := schemaDecoder.Decode(&opt, *val); err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - users := make(Users, 0) - sql := `SELECT id, email, 'password' AS password, name, role, - created_at, updated_at, deleted_at - FROM users - WHERE verified IS TRUE - AND deleted_at IS NULL;` - if err := DBH.Select(&users, sql); err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - return &users, nil -} - -func (u UserService) get(id int64, dummy string, claims *Claims) (entity, *appError) { - user, err := dbGetUserById(id) - user.Password = "" - if err != nil { - return nil, newJSONError(err, http.StatusInternalServerError) - } - - user.CanEdit = claims.Role == "A" || id == claims.Sub - - payload := UserPayload{ - User: user, - Meta: &UserMeta{ - CanAdd: claims.Role == "A", - }, - } - return &payload, nil -} - -func (u UserService) update(id int64, e *entity, dummy string, claims *Claims) *appError { - user := (*e).(*UserPayload).User - - original_user, err := dbGetUserById(id) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - user.Id = id - user.Password = original_user.Password - user.Verified = original_user.Verified - user.UpdatedAt = currentTime() - - if err := user.validate(); err != nil { - return &appError{Error: err, Status: StatusUnprocessableEntity} - } - - count, err := DBH.Update(user) - user.Password = "" - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - if count != 1 { - return ErrUserNotUpdatedJSON - } - - return nil -} - -func (u UserService) create(e *entity, dummy string, claims *Claims) *appError { - user := (*e).(*UserPayload).User - if err := user.validate(); err != nil { - return &appError{Error: err, Status: StatusUnprocessableEntity} - } - ct := currentTime() - user.CreatedAt = ct - user.UpdatedAt = ct - hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - user.Password = string(hash) - user.Role = "R" - user.Verified = false - - if err := DBH.Insert(user); err != nil { - if err, ok := err.(*pq.Error); ok { - if err.Code == "23505" { - return ErrEmailAddressTakenJSON - } - } - return newJSONError(err, http.StatusInternalServerError) - } - - user.Password = "password" // don't want to send the hashed PW back to the client - - q := `INSERT INTO verification (user_id, nonce, referer, created_at) VALUES ($1, $2, $3, $4);` - nonce, err := generateNonce() - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - _, err = DBH.Exec(q, user.Id, nonce, claims.Ref, ct) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - // Send out confirmation email - mg, ok := mgAccts[claims.Ref] - if ok { - sender := fmt.Sprintf("%s Admin ", mg.Domain(), mg.Domain()) - recipient := fmt.Sprintf("%s <%s>", user.Name, user.Email) - subject := fmt.Sprintf("New Account Confirmation - %s", mg.Domain()) - message := fmt.Sprintf("You are receiving this message because this email "+ - "address was used to sign up for an account at %s. Please visit this "+ - "URL to complete the sign up process: %s/users/new/verify/%s. If you "+ - "did not request an account, please disregard this message.", - mg.Domain(), claims.Ref, nonce) - m := mailgun.NewMessage(sender, subject, message, recipient) - _, _, err := mg.Send(m) - if err != nil { - log.Printf("%+v\n", err) - return newJSONError(err, http.StatusInternalServerError) - } - } - - return nil -} - -// for thermokarst/jwt: authentication callback -func dbAuthenticate(email string, password string) error { - var user User - q := `SELECT * - FROM users - WHERE lower(email)=lower($1) - AND verified IS TRUE - AND deleted_at IS NULL;` - if err := DBH.SelectOne(&user, q, email); err != nil { - return ErrInvalidEmailOrPassword - } - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - return ErrInvalidEmailOrPassword - } - return nil -} - -func dbGetUserById(id int64) (*User, error) { - var user User - q := `SELECT * - FROM users - WHERE id=$1 - AND verified IS TRUE - AND deleted_at IS NULL;` - if err := DBH.SelectOne(&user, q, id); err != nil { - if err == sql.ErrNoRows { - return nil, ErrUserNotFound - } - return nil, err - } - return &user, nil -} - -// for thermokarst/jwt: setting user in claims bundle -func dbGetUserByEmail(email string) (*User, error) { - var user User - q := `SELECT * - FROM users - WHERE lower(email)=lower($1) - AND verified IS TRUE - AND deleted_at IS NULL;` - if err := DBH.SelectOne(&user, q, email); err != nil { - if err == sql.ErrNoRows { - return nil, ErrUserNotFound - } - return nil, err - } - return &user, nil -} - -func handleUserVerify(w http.ResponseWriter, r *http.Request) *appError { - nonce := mux.Vars(r)["Nonce"] - q := `SELECT user_id, referer FROM verification WHERE nonce=$1;` - - var ver struct { - User_id int64 - Referer string - } - if err := DBH.SelectOne(&ver, q, nonce); err != nil { - log.Print(err) - return newJSONError(err, http.StatusInternalServerError) - } - - if ver.User_id == 0 { - return newJSONError(errors.New("No user found"), http.StatusInternalServerError) - } - - var user User - if err := DBH.Get(&user, ver.User_id); err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - - user.UpdatedAt = currentTime() - user.Verified = true - - count, err := DBH.Update(&user) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - if count != 1 { - return newJSONError(errors.New("Count 0"), http.StatusInternalServerError) - } - - q = `DELETE FROM verification WHERE user_id=$1;` - _, err = DBH.Exec(q, user.Id) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - fmt.Fprintln(w, `{"msg":"All set! Please log in."}`) - return nil -} - -func handleUserLockout(w http.ResponseWriter, r *http.Request) *appError { - email := r.FormValue("email") - if email == "" { - return newJSONError(errors.New("missing email"), http.StatusInternalServerError) - } - token, err := j.CreateToken(email) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - origin := r.Header.Get("Origin") - hostUrl, err := url.Parse(origin) - if err != nil { - return newJSONError(err, http.StatusInternalServerError) - } - hostUrl.Path += "/users/lockoutauthenticate" - params := url.Values{} - params.Add("token", token) - hostUrl.RawQuery = params.Encode() - - // Send out email - mg, ok := mgAccts[origin] - if ok { - sender := fmt.Sprintf("%s Admin ", mg.Domain(), mg.Domain()) - recipient := fmt.Sprintf("%s", email) - subject := fmt.Sprintf("Password Reset Request - %s", mg.Domain()) - message := fmt.Sprintf("You are receiving this message because this email "+ - "address was used in an account lockout request at %s. Please visit "+ - "this URL to complete the process: %s. If you did not request help "+ - "with a lockout, please disregard this message.", - mg.Domain(), hostUrl.String()) - m := mailgun.NewMessage(sender, subject, message, recipient) - _, _, err := mg.Send(m) - if err != nil { - log.Printf("%+v\n", err) - return newJSONError(err, http.StatusInternalServerError) - } - } - - fmt.Fprintln(w, `{}`) - return nil -}