Refactor previous COOPGO Identity service - Initial commit
This commit is contained in:
120
storage/etcd.go
Normal file
120
storage/etcd.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"go.etcd.io/etcd/client/v3/namespace"
|
||||
)
|
||||
|
||||
type EtcdKVStore struct {
|
||||
Client *clientv3.Client
|
||||
}
|
||||
|
||||
func NewEtcdKVStore(cfg *viper.Viper) (EtcdKVStore, error) {
|
||||
var (
|
||||
endpoints = cfg.GetStringSlice("storage.kv.etcd.endpoints")
|
||||
prefix = cfg.GetString("storage.kv.etcd.prefix")
|
||||
)
|
||||
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: endpoints,
|
||||
DialTimeout: 5 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return EtcdKVStore{}, err
|
||||
}
|
||||
|
||||
cli.KV = namespace.NewKV(cli.KV, prefix)
|
||||
cli.Watcher = namespace.NewWatcher(cli.Watcher, prefix)
|
||||
cli.Lease = namespace.NewLease(cli.Lease, prefix)
|
||||
|
||||
return EtcdKVStore{
|
||||
Client: cli,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s EtcdKVStore) Put(k string, v any) error {
|
||||
// var data bytes.Buffer // Stand-in for a network connection
|
||||
// enc := gob.NewEncoder(&data)
|
||||
// err := enc.Encode(v)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// _, err = s.Client.KV.Put(context.TODO(), k, data.String())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_, err = s.Client.KV.Put(ctx, k, string(data))
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s EtcdKVStore) PutWithTTL(k string, v any, duration time.Duration) error {
|
||||
lease, err := s.Client.Lease.Grant(context.TODO(), int64(duration.Seconds()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// var data bytes.Buffer // Stand-in for a network connection
|
||||
// enc := gob.NewEncoder(&data)
|
||||
// err = enc.Encode(v)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// _, err = s.Client.KV.Put(context.TODO(), k, data.String(), clientv3.WithLease(lease.ID))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_, err = s.Client.KV.Put(ctx, k, string(data), clientv3.WithLease(lease.ID))
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s EtcdKVStore) Get(k string) (any, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
resp, err := s.Client.KV.Get(ctx, k)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range resp.Kvs {
|
||||
var data any
|
||||
// var reader bytes.Buffer
|
||||
// reader.Write(v.Value)
|
||||
// enc := gob.NewDecoder(&reader)
|
||||
// err := enc.Decode(&data)
|
||||
err := json.Unmarshal([]byte(v.Value), &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We return directly as we want to last revision of value
|
||||
return data, nil
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("no value %v", k))
|
||||
}
|
||||
|
||||
func (s EtcdKVStore) Delete(k string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_, err := s.Client.KV.Delete(ctx, k)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
142
storage/mongodb.go
Normal file
142
storage/mongodb.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type MongoDBStorage struct {
|
||||
*mongo.Client
|
||||
DbName string
|
||||
Collections map[string]string
|
||||
}
|
||||
|
||||
func NewMongoDBStorage(cfg *viper.Viper) (MongoDBStorage, error) {
|
||||
var (
|
||||
mongodb_host = cfg.GetString("storage.db.mongodb.host")
|
||||
mongodb_port = cfg.GetString("storage.db.mongodb.port")
|
||||
mongodb_dbname = cfg.GetString("storage.db.mongodb.db_name")
|
||||
mongodb_users = cfg.GetString("storage.db.mongodb.collections.users")
|
||||
)
|
||||
|
||||
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://" + mongodb_host + ":" + mongodb_port))
|
||||
if err != nil {
|
||||
return MongoDBStorage{}, err
|
||||
}
|
||||
|
||||
err = client.Connect(context.TODO())
|
||||
|
||||
if err != nil {
|
||||
return MongoDBStorage{}, err
|
||||
}
|
||||
|
||||
storage := MongoDBStorage{
|
||||
Client: client,
|
||||
DbName: mongodb_dbname,
|
||||
Collections: map[string]string{
|
||||
"users": mongodb_users,
|
||||
},
|
||||
}
|
||||
//TODO Indexes
|
||||
return storage, err
|
||||
}
|
||||
|
||||
func (s MongoDBStorage) GetAccount(id string) (*Account, error) {
|
||||
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
|
||||
|
||||
account := &Account{}
|
||||
if err := collection.FindOne(context.TODO(), bson.M{"_id": id}).Decode(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// LocalAuthentication returns an Account matching with one of username, email or password.
|
||||
// If username, is provided (not an empty string), it will search by username only
|
||||
// If username is an empty string and email is provided, it will search by email
|
||||
// If both username and email are empty strings, phone_number must be provided and it will search by phone number
|
||||
func (s MongoDBStorage) LocalAuthentication(namespace string, username string, email string, phone_number string) (*Account, error) {
|
||||
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
|
||||
|
||||
account := &Account{}
|
||||
|
||||
if username != "" {
|
||||
if err := collection.FindOne(context.TODO(), bson.M{"namespace": namespace, "authentication.local.username": username}).Decode(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if email != "" {
|
||||
if err := collection.FindOne(context.TODO(), bson.M{"namespace": namespace, "authentication.local.email": email}).Decode(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if phone_number != "" {
|
||||
if err := collection.FindOne(context.TODO(), bson.M{"namespace": namespace, "authentication.local.phone_number": phone_number}).Decode(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("missing username, email or password")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s MongoDBStorage) GetAccounts(namespaces []string) (accounts []Account, err error) {
|
||||
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
|
||||
|
||||
var cur *mongo.Cursor
|
||||
|
||||
findOptions := options.Find()
|
||||
|
||||
if len(namespaces) == 0 {
|
||||
cur, err = collection.Find(context.TODO(), bson.D{}, findOptions)
|
||||
if err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
} else {
|
||||
cur, err = collection.Find(context.TODO(), bson.M{"namespace": bson.M{"$in": namespaces}}, findOptions)
|
||||
if err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
}
|
||||
|
||||
for cur.Next(context.TODO()) {
|
||||
var account Account
|
||||
var elem bson.M
|
||||
|
||||
err := cur.Decode(&elem)
|
||||
if err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
bsonBytes, _ := bson.Marshal(elem)
|
||||
bson.Unmarshal(bsonBytes, &account)
|
||||
|
||||
accounts = append(accounts, account)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s MongoDBStorage) CreateAccount(account Account) error {
|
||||
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
|
||||
if _, err := collection.InsertOne(context.TODO(), account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s MongoDBStorage) UpdateAccount(account Account) error {
|
||||
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
|
||||
if _, err := collection.ReplaceOne(context.TODO(), bson.M{"_id": account.ID}, account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
93
storage/storage.go
Normal file
93
storage/storage.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Storage interface
|
||||
type Storage struct {
|
||||
DB DBStorage
|
||||
KV KVStore
|
||||
}
|
||||
|
||||
func NewStorage(cfg *viper.Viper) (Storage, error) {
|
||||
dbstorage, err := NewDBStorage(cfg)
|
||||
if err != nil {
|
||||
return Storage{}, err
|
||||
}
|
||||
kvstore, err := NewKVStore(cfg)
|
||||
if err != nil {
|
||||
return Storage{}, err
|
||||
}
|
||||
return Storage{
|
||||
DB: dbstorage,
|
||||
KV: kvstore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DBStorage interface {
|
||||
GetAccount(id string) (*Account, error)
|
||||
LocalAuthentication(namespace string, username string, email string, phone_number string) (*Account, error)
|
||||
GetAccounts(namespaces []string) ([]Account, error)
|
||||
CreateAccount(account Account) error
|
||||
UpdateAccount(account Account) error
|
||||
}
|
||||
|
||||
func NewDBStorage(cfg *viper.Viper) (DBStorage, error) {
|
||||
var (
|
||||
storage_type = cfg.GetString("storage.db.type")
|
||||
)
|
||||
|
||||
switch storage_type {
|
||||
case "mongodb":
|
||||
s, err := NewMongoDBStorage(cfg)
|
||||
return s, err
|
||||
default:
|
||||
return nil, fmt.Errorf("storage type %v is not supported", storage_type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type KVStore interface {
|
||||
Put(string, any) error
|
||||
PutWithTTL(string, any, time.Duration) error
|
||||
Get(string) (any, error)
|
||||
Delete(string) error
|
||||
}
|
||||
|
||||
func NewKVStore(cfg *viper.Viper) (KVStore, error) {
|
||||
kv, err := NewEtcdKVStore(cfg)
|
||||
return kv, err
|
||||
}
|
||||
|
||||
// Data models
|
||||
|
||||
type Account struct {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
Namespace string `json:"namespace"`
|
||||
Authentication AccountAuth `json:"authentication" bson:"authentication"`
|
||||
Data map[string]any `json:"data"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type AccountAuth struct {
|
||||
Local LocalAuth
|
||||
//TODO handle SSO
|
||||
}
|
||||
|
||||
type LocalAuth struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
EmailValidation Validation `json:"email_validation" bson:"email_validation"`
|
||||
PhoneNumber string `json:"phone_number" bson:"phone_number"`
|
||||
PhoneNumberValidation Validation `json:"phone_number_validation" bson:"phone_number_validation"`
|
||||
}
|
||||
|
||||
type Validation struct {
|
||||
Validated bool
|
||||
ValidationCode string `json:"validation_code" bson:"validation_code"`
|
||||
}
|
||||
Reference in New Issue
Block a user