mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-14 12:04:41 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user