Skip to content

Commit

Permalink
Version 2 API (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
MicahParks authored Oct 30, 2024
1 parent c366a2e commit 9e5818c
Show file tree
Hide file tree
Showing 100 changed files with 3,238 additions and 1,415 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.env
.idea
config.json
config.local.json
delme.*
*.prod.yml
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2022 Micah Parks
Copyright 2024 Micah Parks

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
67 changes: 31 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,59 @@
# magiclinksdev

You can find the documentation for this project on the [docs site](https://docs.magiclinks.dev). This site contains
resources for implementing a client and self-hosting the project.

You can find the SaaS landing page at [https://magiclinks.dev](https://magiclinks.dev). Use of the SaaS platform is not
required, but it's very inexpensive and may be cheaper than deploying yourself.

# Getting started

The **magiclinksdev** project is an authentication service that uses magic links to authenticate users. A typical use
case would involve sending a magic link to a user via email. After the user clicks the link, a new authenticated session
is created for that user. Sometimes **magiclinksdev** is abbreviated as "mld".

## About

This project is a magic link authentication service. It serves use cases like:
The **magiclinksdev** project is an authentication service for magic link and One-Time Password (OTP) use cases. There
is built-in email support through Amazon SES and SendGrid.

Use cases include:
* Sign up
* Log in
* Password resets
* Email verification
* And more authentication use cases

It can be used to supplement password authentication or replace it entirely.
This project can be used to supplement password authentication or replace it entirely.

If your project has an alternate secure means of communication, you can use generate magic links and OTPs without
sending emails. An example would be mobile push notifications.

A typical use case involves sending a magic link to a user via email. After the user clicks the link, a new
authenticated session is created for that user.
## Getting started

To get started implementing a client application that uses **magiclinksdev** for authentication, the recommended path
is:
1. Do the [quickstart](https://docs.magiclinks.dev/self-host-quickstart)
2. Find a [pre-built SDK](https://docs.magiclinks.dev/client-sdk) or [generate one from the formatted API specification](https://docs.magiclinks.dev/client-api-specification#generate-code)
3. Choose the [magic link](https://docs.magiclinks.dev/client-magic-link-workflow) or [OTP](https://docs.magiclinks.dev/client-otp-workflow) workflow
4. Review the [implementation tips](https://docs.magiclinks.dev/client-implementation-tips) for recommendations and best practices

## Screenshots

The built-in email template is populated on a per-request basis. It adapts to the device's theme automatically. This
template was built using [maizzle](https://maizzle.com/).
The built-in email templates are friendly to mobile and desktop screens. They also adapt to light/dark mode
automatically. The templates are built using [maizzle](https://maizzle.com/).

<span>
<img width="400" src="https://magiclinks.dev/screenshots/mobile-light.png" alt="">
<img width="400" src="https://magiclinks.dev/screenshots/mobile-dark.png" alt="">
<img width="400" src="https://magiclinks.dev/screenshots/magic-link-email-light-mobile-example.png" alt=""/>
<img width="400" src="https://magiclinks.dev/screenshots/magic-link-email-dark-mobile-example.png" alt=""/>
</span>
<span>
<img width="400" src="https://magiclinks.dev/screenshots/otp-email-light-mobile-example.png" alt=""/>
<img width="400" src="https://magiclinks.dev/screenshots/otp-email-dark-mobile-example.png" alt=""/>
</span>

## Suggested Email Workflow

<img width="1000" src="https://magiclinks.dev/mermaid/suggested-email-workflow.png" alt=""/>

## Implementing a client application
## Suggested Magic Link Workflow
<img width="1000" src="https://magiclinks.dev/mermaid/suggested-magic-link-workflow.png" alt=""/>

Client applications are programs that use the **magiclinksdev** project to authenticate their users. Check out the
[**SDKs**](https://docs.magiclinks.dev/sdks) page to get started with an existing SDK. If you can't find an SDK for your
language, you can use the [**Specification**](https://docs.magiclinks.dev/specification) to implement your own client by
hand or code generation. To learn more about the client workflow, check out the
[**Workflow**](https://docs.magiclinks.dev/workflow) page.
## Suggested OTP Workflow
<img width="1000" src="https://magiclinks.dev/mermaid/suggested-otp-workflow.png" alt=""/>

## Self-hosting the service

The **magiclinksdev** project can be self-hosted. Check out the [**Quickstart**](https://docs.magiclinks.dev/quickstart)
page to get started in minutes. For reference on configuring your self-hosted instance, check out the
[**Configuration**](https://docs.magiclinks.dev/configuration).
The **magiclinksdev** project is open-source and can be self-hosted. Check out the [**Quickstart**](https://docs.magiclinks.dev/self-host-quickstart) page
to get started in minutes. For reference on configuring your self-hosted instance, check out the
[**Configuration**](https://docs.magiclinks.dev/self-host-configuration).

## Source code and license

The **magiclinksdev** project is [open source on GitHub](https://github.com/MicahParks/magiclinksdev) and licensed
under [Apache License 2.0](https://github.com/MicahParks/magiclinksdev/blob/master/LICENSE).
under [**Apache License 2.0**](https://github.com/MicahParks/magiclinksdev/blob/master/LICENSE).

## Optional SaaS platform

Expand Down
6 changes: 3 additions & 3 deletions buildDocker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ if [ "$1" = "push" ]; then
then
exit 1
fi
docker build --pull --push --file nop.Dockerfile --tag micahparks/magiclinksdevmulti .
docker build --pull --push --file multi.Dockerfile --tag micahparks/magiclinksdevmulti .
docker build --pull --push --file nop.Dockerfile --tag micahparks/magiclinksdevnop .
docker build --pull --push --file ses.Dockerfile --tag micahparks/magiclinksdevses .
docker build --pull --push --file sendgrid.Dockerfile --tag micahparks/magiclinksdevsendgrid .
docker build --pull --push --file nop.Dockerfile --tag "micahparks/magiclinksdevmulti:$TAG" .
docker build --pull --push --file multi.Dockerfile --tag "micahparks/magiclinksdevmulti:$TAG" .
docker build --pull --push --file nop.Dockerfile --tag "micahparks/magiclinksdevnop:$TAG" .
docker build --pull --push --file ses.Dockerfile --tag "micahparks/magiclinksdevses:$TAG" .
docker build --pull --push --file sendgrid.Dockerfile --tag "micahparks/magiclinksdevsendgrid:$TAG" .
else
docker build --pull --file nop.Dockerfile --tag micahparks/magiclinksdevmulti .
docker build --pull --file multi.Dockerfile --tag micahparks/magiclinksdevmulti .
docker build --pull --file nop.Dockerfile --tag micahparks/magiclinksdevnop .
docker build --pull --file ses.Dockerfile --tag micahparks/magiclinksdevses .
docker build --pull --file sendgrid.Dockerfile --tag micahparks/magiclinksdevsendgrid .
Expand Down
69 changes: 48 additions & 21 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import (
)

const (
// SaaSBaseURL is the base URL for the SaaS offering. The SaaS offering is optional and the magiclinksdev project
// can be self-hosted.
// SaaSBaseURL is the base URL for the SaaS platform. The SaaS platform is optional and the magiclinksdev project
// is open-source and can be self-hosted.
SaaSBaseURL = "https://magiclinks.dev"
// SaaSIss is the iss claim for JWTs in the SaaS offering.
SaaSIss = SaaSBaseURL
Expand All @@ -43,7 +43,7 @@ type Options struct {
HTTP *http.Client
}

// Client is a client for the magiclinksdev project.
// Client is the official Golang API client for the magiclinksdev project.
type Client struct {
apiKey uuid.UUID
aud uuid.UUID
Expand All @@ -54,9 +54,9 @@ type Client struct {
}

// New creates a new magiclinksdev client. The apiKey and aud are tied to the service account being used. The baseURL is
// the HTTP(S) location of the magiclinksdev deployment. Only use HTTPS in production. For the SaaS offering, use the
// the HTTP(S) location of the magiclinksdev deployment. Only use HTTPS in production. For the SaaS platform, use the
// SaaSBaseURL constant. The iss is the issuer of the JWTs, which is in the configuration of the magiclinksdev
// deployment. For the SaaS offering, use the SaaSIss constant. Providing an empty string for the iss will disable
// deployment. For the SaaS platform, use the SaaSIss constant. Providing an empty string for the iss will disable
// issuer validation.
func New(apiKey, aud uuid.UUID, baseURL, iss string, options Options) (Client, error) {
u, err := url.Parse(baseURL)
Expand Down Expand Up @@ -96,8 +96,8 @@ func New(apiKey, aud uuid.UUID, baseURL, iss string, options Options) (Client, e
}

// LocalJWTValidate validates a JWT locally. If the claims argument is not nil, its value will be passed directly to
// jwt.ParseWithClaims. The claims should be unmarshalled into the provided non-nil pointer after the function call. See
// the documentation for jwt.ParseWithClaims for more information. Registered JWT claims will be validated regardless if
// jwt.ParseWithClaims. The claims should be unmarshalled into the claims argument if it is a non-nil pointer. See the
// documentation for jwt.ParseWithClaims for more information. Registered JWT claims will be validated regardless if
// claims are specified or not.
func (c Client) LocalJWTValidate(token string, claims jwt.Claims) (*jwt.Token, error) {
if c.keyf == nil {
Expand Down Expand Up @@ -136,15 +136,6 @@ func (c Client) LocalJWTValidate(token string, claims jwt.Claims) (*jwt.Token, e
return t, nil
}

// EmailLinkCreate calls the /email-link/create endpoint and returns the appropriate response.
func (c Client) EmailLinkCreate(ctx context.Context, req model.EmailLinkCreateRequest) (model.EmailLinkCreateResponse, model.Error, error) {
resp, errResp, err := request[model.EmailLinkCreateRequest, model.EmailLinkCreateResponse](ctx, c, http.StatusCreated, network.PathEmailLinkCreate, req)
if err != nil {
return model.EmailLinkCreateResponse{}, errResp, fmt.Errorf("failed to create email link: %w", err)
}
return resp, errResp, nil
}

// JWTCreate calls the /jwt/create endpoint and returns the appropriate response.
func (c Client) JWTCreate(ctx context.Context, req model.JWTCreateRequest) (model.JWTCreateResponse, model.Error, error) {
resp, errResp, err := request[model.JWTCreateRequest, model.JWTCreateResponse](ctx, c, http.StatusCreated, network.PathJWTCreate, req)
Expand All @@ -165,16 +156,52 @@ func (c Client) JWTValidate(ctx context.Context, req model.JWTValidateRequest) (
return resp, errResp, nil
}

// LinkCreate calls the /link/create endpoint and returns the appropriate response.
func (c Client) LinkCreate(ctx context.Context, req model.LinkCreateRequest) (model.LinkCreateResponse, model.Error, error) {
resp, errResp, err := request[model.LinkCreateRequest, model.LinkCreateResponse](ctx, c, http.StatusCreated, network.PathLinkCreate, req)
// MagicLinkCreate calls the /magic-link/create endpoint and returns the appropriate response.
func (c Client) MagicLinkCreate(ctx context.Context, req model.MagicLinkCreateRequest) (model.MagicLinkCreateResponse, model.Error, error) {
resp, errResp, err := request[model.MagicLinkCreateRequest, model.MagicLinkCreateResponse](ctx, c, http.StatusCreated, network.PathMagicLinkCreate, req)
if err != nil {
return model.MagicLinkCreateResponse{}, errResp, fmt.Errorf("failed to create link: %w", err)
}
return resp, errResp, nil
}

// MagicLinkEmailCreate calls the /magic-link-email/create endpoint and returns the appropriate response.
func (c Client) MagicLinkEmailCreate(ctx context.Context, req model.MagicLinkEmailCreateRequest) (model.MagicLinkEmailCreateResponse, model.Error, error) {
resp, errResp, err := request[model.MagicLinkEmailCreateRequest, model.MagicLinkEmailCreateResponse](ctx, c, http.StatusCreated, network.PathMagicLinkEmailCreate, req)
if err != nil {
return model.MagicLinkEmailCreateResponse{}, errResp, fmt.Errorf("failed to create email link: %w", err)
}
return resp, errResp, nil
}

// OTPCreate calls the /otp/create endpoint and returns the appropriate response.
func (c Client) OTPCreate(ctx context.Context, req model.OTPCreateRequest) (model.OTPCreateResponse, model.Error, error) {
resp, errResp, err := request[model.OTPCreateRequest, model.OTPCreateResponse](ctx, c, http.StatusCreated, network.PathOTPCreate, req)
if err != nil {
return model.OTPCreateResponse{}, errResp, fmt.Errorf("failed to create OTP: %w", err)
}
return resp, errResp, nil
}

// OTPValidate calls the /otp/validate endpoint and returns the appropriate response.
func (c Client) OTPValidate(ctx context.Context, req model.OTPValidateRequest) (model.OTPValidateResponse, model.Error, error) {
resp, errResp, err := request[model.OTPValidateRequest, model.OTPValidateResponse](ctx, c, http.StatusOK, network.PathOTPValidate, req)
if err != nil {
return model.OTPValidateResponse{}, errResp, fmt.Errorf("failed to validate OTP: %w", err)
}
return resp, errResp, nil
}

// OTPEmailCreate calls the /otp-email/create endpoint and returns the appropriate response.
func (c Client) OTPEmailCreate(ctx context.Context, req model.OTPEmailCreateRequest) (model.OTPEmailCreateResponse, model.Error, error) {
resp, errResp, err := request[model.OTPEmailCreateRequest, model.OTPEmailCreateResponse](ctx, c, http.StatusCreated, network.PathOTPEmailCreate, req)
if err != nil {
return model.LinkCreateResponse{}, errResp, fmt.Errorf("failed to create link: %w", err)
return model.OTPEmailCreateResponse{}, errResp, fmt.Errorf("failed to create email OTP: %w", err)
}
return resp, errResp, nil
}

// Ready calls the /ready endpoint. An error is returned if the service is not ready.
// Ready calls the /ready endpoint. An error is returned if the service is not ready to accept requests.
func (c Client) Ready(ctx context.Context) error {
u, err := c.baseURL.Parse(network.PathReady)
if err != nil {
Expand Down
Loading

0 comments on commit 9e5818c

Please sign in to comment.