Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
229
phone.go7.06 kB
package phone import ( "context" "fmt" "log/slog" "github.com/Southclaws/fault" "github.com/Southclaws/fault/fctx" "github.com/Southclaws/fault/fmsg" "github.com/Southclaws/fault/ftag" "github.com/Southclaws/opt" "github.com/rs/xid" "github.com/samber/lo" "github.com/Southclaws/storyden/app/resources/account" "github.com/Southclaws/storyden/app/resources/account/account_querier" "github.com/Southclaws/storyden/app/resources/account/account_writer" "github.com/Southclaws/storyden/app/resources/account/authentication" "github.com/Southclaws/storyden/app/resources/settings" "github.com/Southclaws/storyden/app/services/account/register" "github.com/Southclaws/storyden/app/services/authentication/provider" "github.com/Southclaws/storyden/internal/infrastructure/sms" "github.com/Southclaws/storyden/internal/otp" ) var ( errHandleMismatch = fault.New("phone already linked to different account") errNoPhoneAuth = fault.New("no phone auth method linked to account") errNotFound = fault.New("account not found") errOneTimeCodeMismatch = fault.New("one time code mismatch") ) var ( requiredMode = authentication.ModePhone service = authentication.ServicePhoneVerify tokenType = authentication.TokenTypeNone ) const template = `Your unique one-time login code is: %s` type Provider struct { logger *slog.Logger settings *settings.SettingsRepository auth authentication.Repository account *account_querier.Querier register *register.Registrar sms sms.Sender } func New( logger *slog.Logger, settings *settings.SettingsRepository, auth authentication.Repository, account *account_querier.Querier, register *register.Registrar, sms sms.Sender, ) *Provider { return &Provider{ logger: logger, settings: settings, auth: auth, account: account, register: register, sms: sms, } } func (p *Provider) Service() authentication.Service { return service } func (p *Provider) Token() authentication.TokenType { return tokenType } func (p *Provider) Enabled(ctx context.Context) (bool, error) { settings, err := p.settings.Get(ctx) if err != nil { return false, fault.Wrap(err, fctx.With(ctx)) } return settings.AuthenticationMode.Or(authentication.ModeHandle) == requiredMode, nil } func (p *Provider) Register(ctx context.Context, handle string, phone string, inviteCode opt.Optional[xid.ID]) (*account.Account, error) { if err := provider.CheckMode(ctx, p.logger, p.settings, requiredMode); err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } // // STEP 1. // // Using the provided phone number, look up an authentication record which // points to an account already registered with the system. We need to do // this because there's no separation between registration and login via the // phone login system so if there's an account already, we start auth again. // authrecord, exists, err := p.auth.LookupByIdentifier(ctx, service, phone) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.With("failed to get account")) } var acc *account.Account if exists { if err := acc.RejectSuspended(); err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } acc = &authrecord.Account if acc.Handle != handle { return nil, fault.Wrap(errHandleMismatch, fctx.With(ctx), ftag.With(ftag.PermissionDenied), fmsg.WithDesc("handle mismatch", "Phone number already registered to a different account."), ) } // // STEP 1.5: // // If an account already exists, there's a chance the account also has a // phone authentication record associated with it. Currently, we only // support a single phone associated with an account so if there is one, // it needs to be deleted so it can be created again with a new code. // auths, err := p.auth.GetAuthMethods(ctx, acc.ID) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } // If there's already a phone auth associated with the account, deleted it // and start fresh with the new request. // NOTE: This could result in a DoS for the account holder... if _, exists = lo.Find(auths, func(a *authentication.Authentication) bool { return a.Service == service }); exists { _, err = p.auth.Delete(ctx, acc.ID, phone, service) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } } } else { // // If there isn't an account already with this phone number, we create // a new one using the @handle specified in the request. // opts := []account_writer.Option{} inviteCode.Call(func(id xid.ID) { opts = append(opts, account_writer.WithInvitedBy(id)) }) acc, err = p.register.Create(ctx, opt.New(handle), opts...) if err != nil { if ftag.Get(err) == ftag.AlreadyExists { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.With("failed to create account"), fmsg.WithDesc("already exists", "Handle already registered with a different authentication method.")) } return nil, fault.Wrap(err, fctx.With(ctx), fmsg.With("failed to create account")) } } // // STEP 2: // // Generate a one-time-password which is a 6 digit number and send this to // the phone number specified in the request. // code, err := otp.Generate() if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.With("failed to generate code")) } _, err = p.auth.Create(ctx, acc.ID, service, authentication.TokenTypeNone, phone, code, nil) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.With("failed to create account authentication instance")) } // TODO: For whitelabling, allow the instance brand name to be specified in // the message template. So the message says "Log in to Acme with xyz..." message := fmt.Sprintf(template, code) err = p.sms.Send(ctx, phone, message) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } return acc, nil } func (b *Provider) Link(_ string) (string, error) { // Phone provider does not use external links. return "", nil } func (p *Provider) Login(ctx context.Context, handle string, onetimecode string) (*account.Account, error) { acc, exists, err := p.account.LookupByHandle(ctx, handle) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } if !exists { return nil, fault.Wrap(errNotFound, fctx.With(ctx), ftag.With(ftag.NotFound), fmsg.WithDesc("not found", "No account was found with the provided handle.")) } if err := acc.RejectSuspended(); err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } auths, err := p.auth.GetAuthMethods(ctx, acc.ID) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } phoneauth, exists := lo.Find(auths, func(a *authentication.Authentication) bool { return a.Service == service }) if !exists { return nil, fault.Wrap(errNoPhoneAuth) } if phoneauth.Token != onetimecode { return nil, fault.Wrap(errOneTimeCodeMismatch, fctx.With(ctx), ftag.With(ftag.PermissionDenied), fmsg.WithDesc("mismatch", "The code did not match."), ) } return &acc.Account, nil }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server