Passwordless (#292)

* add two rpcs to User service:
	- Passwordless: endpoint that receives an email, topic and an optional message
	- PasswordlessML: endpoint that receives token and topic via MagicLink.

* Proposal to add Passwordless login feature to the endpoint user.

* remove currency run check

* Commit from GitHub Actions (Publish APIs & Clients)

* Create downstream.yml

* Commit from GitHub Actions (Publish APIs & Clients)

* update todo

* Commit from GitHub Actions (Publish APIs & Clients)

* Update publish.yml

* Commit from GitHub Actions (Publish APIs & Clients)

* Update publish.yml

* Commit from GitHub Actions (Publish APIs & Clients)

* Update and rename publish.yml to generate.yml

* Update generate.yml

* Commit from GitHub Actions (Generate Clients & Examples)

* Commit from GitHub Actions (Generate Clients & Examples)

* add comments

* Commit from GitHub Actions (Generate Clients & Examples)

* move otp to auth category

* charge for user verification

* Commit from GitHub Actions (Generate Clients & Examples)

* Update user.proto

* Commit from GitHub Actions (Generate Clients & Examples)

* Commit from GitHub Actions (Generate Clients & Examples)

* Change js client git repo url (#249)

* Commit from GitHub Actions (Generate Clients & Examples)

* use tableName func for Count

* Commit from GitHub Actions (Generate Clients & Examples)

* update notes example

* Commit from GitHub Actions (Generate Clients & Examples)

* Update .gitignore

* Update .gitignore

* Commit from GitHub Actions (Generate Clients & Examples)

* add new endpoints SendMagicLink and VerifyToken to M3O user serivce

Signed-off-by: Daniel Joudat <danieljoudat@gmail.com>

* Update user.proto

* add examples to examples.json | convert isvalid to is_valid | add some extra comments in user.proto

Signed-off-by: Daniel Joudat <danieljoudat@gmail.com>

Co-authored-by: Asim Aslam <asim@aslam.me>
Co-authored-by: asim <asim@users.noreply.github.com>
Co-authored-by: Janos Dobronszki <dobronszki@gmail.com>
Co-authored-by: crufter <crufter@users.noreply.github.com>
This commit is contained in:
Daniel Joudat
2021-12-10 12:57:00 +03:00
committed by GitHub
parent f82bc634c1
commit 1d6234155a
6 changed files with 827 additions and 99 deletions

View File

@@ -4,15 +4,20 @@ import (
goctx "context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"path"
"regexp"
"strings"
"time"
"github.com/asim/mq/broker"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
db "github.com/micro/services/db/proto"
otp "github.com/micro/services/otp/proto"
"github.com/micro/services/pkg/tenant"
"github.com/micro/services/user/domain"
pb "github.com/micro/services/user/proto"
"golang.org/x/crypto/bcrypt"
@@ -257,6 +262,10 @@ func (s *User) VerifyEmail(ctx context.Context, req *pb.VerifyEmailRequest, rsp
// mark user as verified
user, err := s.domain.Read(ctx, userId)
if err != nil {
return err
}
user.Verified = true
// update the user
@@ -391,3 +400,145 @@ func (s *User) List(ctx goctx.Context, request *pb.ListRequest, response *pb.Lis
}
return nil
}
func (s *User) SendMagicLink(ctx context.Context, req *pb.SendMagicLinkRequest, stream pb.User_SendMagicLinkStream) error {
// check if the email has the correct format
if !emailFormat.MatchString(req.Email) {
return errors.BadRequest("SendMagicLink.email-format-check", "email has wrong format")
}
// check if the email exist in the DB
users, err := s.domain.Search(ctx, "", req.Email)
if err.Error() == "not found" {
return errors.BadRequest("SendMagicLink.email-check", "email doesn't exist")
} else if err != nil {
return errors.BadRequest("SendMagicLink.email-check", err.Error())
}
// create a token object
token := random(128)
// set ttl to 60 seconds
ttl := 60
// uuid part of the topic
topic := uuid.New().String()
// save token, so we can retrieve it later
err = s.domain.CacheToken(ctx, token, topic, req.Email, ttl)
if err != nil {
return errors.BadRequest("SendMagicLink.cacheToken", "Oooops something went wrong")
}
// send magic link to email address
err = s.domain.SendMLE(req.FromName, req.Email, users[0].Username, req.Subject, req.TextContent, token, req.Address, req.Endpoint)
if err != nil {
return errors.BadRequest("SendMagicLink.sendEmail", "Oooops something went wrong")
}
// subscribe to the topic
id, ok := tenant.FromContext(ctx)
if !ok {
id = "default"
}
// create tenant based topics
topic = path.Join("stream", id, topic)
logger.Infof("Tenant %v subscribing to %v\n", id, topic)
sub, err := broker.Subscribe(topic)
if err != nil {
return errors.InternalServerError("SendMagicLink.subscribe", "failed to subscribe to topic")
}
defer broker.Unsubscribe(topic, sub)
// range over the messages until the subscriber is closed
for msg := range sub {
// unmarshal the message into a struct
s := &pb.Session{}
err = json.Unmarshal(msg, s)
if err != nil {
return errors.InternalServerError("SendMgicLink.unmarshal", "faild to unmarshal the message")
}
if err := stream.Send(&pb.SendMagicLinkResponse{
Session: s,
}); err != nil {
return err
}
}
return nil
}
func (s *User) VerifyToken(ctx context.Context, req *pb.VerifyTokenRequest, rsp *pb.VerifyTokenResponse) error {
// extract token
token := req.Token
// check if token is valid
topic, email, err := s.domain.CacheReadToken(ctx, token)
if err.Error() == "token not found" {
rsp.IsValid = false
rsp.Message = err.Error()
return nil
} else if err.Error() == "token expired" {
rsp.IsValid = false
rsp.Message = err.Error()
return nil
} else if err.Error() == "token empty" {
rsp.IsValid = false
rsp.Message = err.Error()
return nil
} else if err != nil {
return errors.BadRequest("VerifyToken.CacheReadToken", err.Error())
}
// save session
accounts, err := s.domain.Search(ctx, "", email)
if err != nil {
return err
}
if len(accounts) == 0 {
rsp.IsValid = false
rsp.Message = "account not found"
return nil
}
sess := &pb.Session{
Id: random(128),
Created: time.Now().Unix(),
Expires: time.Now().Add(time.Hour * 24 * 7).Unix(),
UserId: accounts[0].Id,
}
if err := s.domain.CreateSession(ctx, sess); err != nil {
return errors.InternalServerError("VerifyToken.createSession", err.Error())
}
// publish a message which holds the session value.
// get the tenant
id, ok := tenant.FromContext(ctx)
if !ok {
id = "default"
}
// create tenant based topics
topic = path.Join("stream", id, topic)
// marshal the data
b, _ := json.Marshal(sess)
logger.Infof("Tenant %v publishing to %v\n", id, topic)
// publish the message
err = broker.Publish(topic, b)
if err != nil {
return errors.InternalServerError("VerifyToken.publish", "Ooops something went wrong, please try again")
}
rsp.IsValid = true
rsp.Message = ""
return nil
}