|
@@ -26,49 +26,212 @@
|
|
|
package auth
|
|
|
|
|
|
import (
|
|
|
+ "context"
|
|
|
+ "errors"
|
|
|
"log"
|
|
|
+ "time"
|
|
|
|
|
|
- db "git.semlanik.org/semlanik/gostfix/db"
|
|
|
+ "git.semlanik.org/semlanik/gostfix/config"
|
|
|
utils "git.semlanik.org/semlanik/gostfix/utils"
|
|
|
uuid "github.com/google/uuid"
|
|
|
+ "go.mongodb.org/mongo-driver/bson"
|
|
|
+ "go.mongodb.org/mongo-driver/mongo"
|
|
|
+ "go.mongodb.org/mongo-driver/mongo/options"
|
|
|
+ "golang.org/x/crypto/bcrypt"
|
|
|
)
|
|
|
|
|
|
type Authenticator struct {
|
|
|
- storage *db.Storage
|
|
|
+ db *mongo.Database
|
|
|
+ usersCollection *mongo.Collection
|
|
|
+ tokensCollection *mongo.Collection
|
|
|
}
|
|
|
|
|
|
-func NewAuthenticator() (a *Authenticator) {
|
|
|
- storage, err := db.NewStorage()
|
|
|
+type Privileges int
|
|
|
|
|
|
+const (
|
|
|
+ AdminPrivilege = 1 << iota
|
|
|
+ SendMailPrivilege
|
|
|
+)
|
|
|
+
|
|
|
+func NewAuthenticator() (*Authenticator, error) {
|
|
|
+ fullUrl := "mongodb://"
|
|
|
+ if config.ConfigInstance().MongoUser != "" {
|
|
|
+ fullUrl += config.ConfigInstance().MongoUser
|
|
|
+ if config.ConfigInstance().MongoPassword != "" {
|
|
|
+ fullUrl += ":" + config.ConfigInstance().MongoPassword
|
|
|
+ }
|
|
|
+ fullUrl += "@"
|
|
|
+ }
|
|
|
+
|
|
|
+ fullUrl += config.ConfigInstance().MongoAddress
|
|
|
+
|
|
|
+ client, err := mongo.NewClient(options.Client().ApplyURI(fullUrl))
|
|
|
if err != nil {
|
|
|
- log.Fatalf("Unable to intialize user storage %s", err)
|
|
|
- return nil
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
|
+ defer cancel()
|
|
|
+
|
|
|
+ err = client.Connect(ctx)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ db := client.Database("gostfix")
|
|
|
+ a := &Authenticator{
|
|
|
+ db: db,
|
|
|
+ usersCollection: db.Collection("users"),
|
|
|
+ tokensCollection: db.Collection("tokens"),
|
|
|
+ }
|
|
|
+ return a, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (a *Authenticator) CheckUser(user, password string) error {
|
|
|
+ log.Printf("Check user: %s", user)
|
|
|
+ result := struct {
|
|
|
+ User string
|
|
|
+ Password string
|
|
|
+ }{}
|
|
|
+ err := a.usersCollection.FindOne(context.Background(), bson.M{"user": user}).Decode(&result)
|
|
|
+ if err != nil {
|
|
|
+ return errors.New("Invalid user or password")
|
|
|
+ }
|
|
|
+
|
|
|
+ if bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(password)) != nil {
|
|
|
+ return errors.New("Invalid user or password")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (a *Authenticator) addToken(user, token string) error {
|
|
|
+ log.Printf("Add token: %s\n", user)
|
|
|
+ a.tokensCollection.UpdateOne(context.Background(),
|
|
|
+ bson.M{"user": user},
|
|
|
+ bson.M{
|
|
|
+ "$addToSet": bson.M{
|
|
|
+ "token": bson.M{
|
|
|
+ "token": token,
|
|
|
+ "expire": time.Now().Add(time.Hour * 24).Unix(),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ options.Update().SetUpsert(true))
|
|
|
+ a.cleanupTokens(user)
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (a *Authenticator) cleanupTokens(user string) {
|
|
|
+ log.Printf("Cleanup tokens: %s\n", user)
|
|
|
+
|
|
|
+ cur, err := a.tokensCollection.Aggregate(context.Background(),
|
|
|
+ bson.A{
|
|
|
+ bson.M{"$match": bson.M{"user": user}},
|
|
|
+ bson.M{"$unwind": "$token"},
|
|
|
+ })
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalln(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ type tokenMetadata struct {
|
|
|
+ Expire int64
|
|
|
+ Token string
|
|
|
}
|
|
|
|
|
|
- a = &Authenticator{
|
|
|
- storage: storage,
|
|
|
+ tokensToKeep := bson.A{}
|
|
|
+ defer cur.Close(context.Background())
|
|
|
+ for cur.Next(context.Background()) {
|
|
|
+ result := struct {
|
|
|
+ Token *tokenMetadata
|
|
|
+ }{
|
|
|
+ Token: &tokenMetadata{},
|
|
|
+ }
|
|
|
+
|
|
|
+ err = cur.Decode(&result)
|
|
|
+ if err == nil && result.Token.Expire >= time.Now().Unix() {
|
|
|
+ tokensToKeep = append(tokensToKeep, result.Token)
|
|
|
+ } else {
|
|
|
+ log.Printf("Expired token found for %s : %d", user, result.Token.Expire)
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ _, err = a.tokensCollection.UpdateOne(context.Background(), bson.M{"user": user}, bson.M{"$set": bson.M{"token": tokensToKeep}})
|
|
|
return
|
|
|
}
|
|
|
|
|
|
-func (a *Authenticator) Authenticate(user, password string) (string, bool) {
|
|
|
+func (a *Authenticator) Login(user, password string) (string, bool) {
|
|
|
if !utils.RegExpUtilsInstance().EmailChecker.MatchString(user) {
|
|
|
return "", false
|
|
|
}
|
|
|
|
|
|
- if a.storage.CheckUser(user, password) != nil {
|
|
|
+ if a.CheckUser(user, password) != nil {
|
|
|
return "", false
|
|
|
}
|
|
|
|
|
|
token := uuid.New().String()
|
|
|
- a.storage.AddToken(user, token)
|
|
|
+ a.addToken(user, token)
|
|
|
return token, true
|
|
|
}
|
|
|
|
|
|
+func (a *Authenticator) Logout(user, token string) error {
|
|
|
+ a.cleanupTokens(user)
|
|
|
+
|
|
|
+ _, err := a.tokensCollection.UpdateOne(context.Background(), bson.M{"user": user}, bson.M{"$pull": bson.M{"token": bson.M{"token": token}}})
|
|
|
+ if err != nil {
|
|
|
+ log.Printf("Unable to remove token %s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+func (a *Authenticator) checkToken(user, token string) error {
|
|
|
+ if token == "" {
|
|
|
+ return errors.New("Invalid token")
|
|
|
+ }
|
|
|
+
|
|
|
+ cur, err := a.tokensCollection.Aggregate(context.Background(),
|
|
|
+ bson.A{
|
|
|
+ bson.M{"$match": bson.M{"user": user}},
|
|
|
+ bson.M{"$unwind": "$token"},
|
|
|
+ bson.M{"$match": bson.M{"token.token": token}},
|
|
|
+ })
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalln(err)
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ ok := false
|
|
|
+ defer cur.Close(context.Background())
|
|
|
+ if cur.Next(context.Background()) {
|
|
|
+ result := struct {
|
|
|
+ Token struct {
|
|
|
+ Expire int64
|
|
|
+ }
|
|
|
+ }{}
|
|
|
+
|
|
|
+ err = cur.Decode(&result)
|
|
|
+
|
|
|
+ ok = err == nil && result.Token.Expire >= time.Now().Unix()
|
|
|
+ }
|
|
|
+
|
|
|
+ if ok {
|
|
|
+ //TODO: Renew token
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return errors.New("Token expired")
|
|
|
+}
|
|
|
+
|
|
|
func (a *Authenticator) Verify(user, token string) bool {
|
|
|
if !utils.RegExpUtilsInstance().EmailChecker.MatchString(user) {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
- return a.storage.CheckToken(user, token) == nil
|
|
|
+ return a.checkToken(user, token) == nil
|
|
|
+}
|
|
|
+
|
|
|
+func (a *Authenticator) CheckPrivileges(user string, privilege Privileges) {
|
|
|
+
|
|
|
}
|