NumValidate is a phone validation REST API powered by Google LibPhoneNumber, a phone number formatting and parsing library released by Google, originally developed for (and currently used in) Google's Android mobile phone operating system, which uses several rigorous rules for parsing, formatting, and validating phone numbers for all countries/regions of the world.
NumValidate is currently out of service becacuse in December 2017 Auth0 has stopped supporting the core API authorization mechanism used by the app (querying for users by app_metadata
).
From my understanding the Auth0 team is not planning to re-introduce this feature anymore.
In this repository you'll be able to find everything you'll need to setup your own NumValidate platform.
Even if you're not interested in phone number validation, I suggest you to take a look around here, since you can easily customize NumValidate to expose any kind of API you like: the phone validation APIs consists in > 200 line of codes, while the remaining code supports the authentication, authorization, plan subscription, plan payment and API tokens management.
The client is a React application that exposes the Home page and the Dashboard: both pages are rendered server-side thanks to Next.To take advantage of Next server-side rendering, the app follows the convention of grouping the main routes under the /pages
directory and putting all the static assets under statics
.
The app itself is not complex at all: the Home page is just a simple page that emphasizes the product features, while the Dashboard (available after a successfull signup) is used for generating API tokens and updating the user subscription plan.
I tried to mimic the structure promoted by create-react-app
as much as possible (since I love it for smaller sites), so I use plain CSS to style the components (with a touch of CSS just for supporting CSS Custom Variables for color names) and I don't use bleeding edge stuff like decorators et similia.
For the same reason, you won't find any state management library here, since setState
has proven to be more than than enough for this project.
client
├── components // The building blocks of the UI
│ ├── Button.css
│ ├── Button.js
│ ├── DashboardCreditCard.css
│ ├── DashboardCreditCard.js
│ └── ...
│
├── config
│ ├── keys.js // Constants used across the app (mostly are env vars)
│ └── strings.js // Almost every string used in the Dashboard
│
├── pages // The actual pages (aka containers) server by Next
│ ├── Dashboard.css
│ ├── Dashboard.js
│ ├── Home.css
│ └── Home.js
│
├── services
│ ├── analytics.js // Simple wrapper over Google Analytics
│ ├── auth.js // Auth0 APIs
│ └── backend.js // Backend (server) APIs
│
├── static // Static assets (favicons, images, robots.txt)
│
├── utils // Common utils used in all the app
│
├── colors.css // CSS colors variables
│
├── globals.css // Global styles
│
├── next.config // Babel config injected into Next
│
├── postcss.config // PostCSS config injected into Next
│
└── types.js // Flowtype type definitions
The server is a Node application powered by Koa, responsible of handling all the API requests and rendering the website/dashboard.
There is no database here: all the users info like the API tokens and the Stripe customer ID are stored in Auth0 in the user appMetadata
. The endpoints defined in routes/user.js
handles all the requests made by the Dashboard to manage the user info.
To fetch and update the users info from Auth0 and validate the user JWT I use an Auth0 API (defined in services/auth0.js
) with granted permissions to many Auth0 management API endpoints.
The requests to the /api
endpoints are rate limited by IP (for unauthenticated users) and by API token (for authenticated users).
The most interesting part of the server is probably the API token rate limiting and caching, which grants a fast response time on consecutive requests. The flow is the following:
/api
endpoint with an x-api-token
header.middlewares/checkApiToken.js
:
middlewares/checkMaxRequests.js
:
/api
endpoint and returns a 429
status code instead (middlewares/rateLimited.js
).The Redis cache expires after the milliseconds defined in the REDIS_CACHE_EXPIRY_IN_MS
environment variable and after an user subscription plan change.
server
├── config
│ ├── keys.js // Constants used across the app (mostly are env vars)
│ └── strings.js // Almost every string used in the Dashboard
│
├── middlewares
│ ├── allowCrossDomain.js // CORS setup
│ ├── auth0TokenGenerator.js // Daily Auth0 Management API token generator
│ ├── checkApiToken.js // Validates the request API token
│ ├── checkAuthToken.js // Validates the request Auth0 JWT
│ ├── checkMaxRequests.js // Checks the max requests limit of the user
│ ├── checkStripeCustomer.js // Verifies that the user initialized in Stripe
│ ├── errorHandler.js // Returns a clean response error
│ ├── fetchUserFromAuth0.js // Given the Auth0 JWT gets the user from Auth0
│ ├── getIpAddress.js // Gets the request IP address
│ ├── rateLimiter.js // Blocks the request on max requests limit reached
│ └── requestLogger.js // Logs the request on console/Papertrail/Sentry
│
├── routes
│ ├── api.js // Phone validation endpoints
│ └── user.js // Dashboard endpoints
│
├── services
│ ├── auth0.js // Auth0 APIs wrapper
│ ├── redis.js // Redis queries
│ ├── sentry.js // Sentry APIs wrapper
│ └── stripe.js // Stripe APIs wrapper
│
├── static
│ └── countries.json // Phone validation supported countries
│
├── utils
│ ├── common.js // Common utils used in all the app
│ └── logger.js // Winston logger setup
│
├── utils // Common utils used in all the app
│
├── app.js // App setup
│
└── index.j // App entry point
To run this project you'll need an Auth0 account.
Since this is a complex process, I'll detail it by using the naming convention I followed with NumValidate, by supposing that your app name is "SuperApp"
Please make sure all the items in the following checklist are marked before running this project in development:
If you're ready for production, you'll need to replicate all the above stuff in a new tenant (named superapp) and also check the following:
I also suggest adding a custom rule for locking the user out of your app until it has not verified its email (it will still be able to access the app for the first day post-signup):
function (user, context, callback) {
var oneDayPostSignup = new Date(user.created_at).getTime() + (24 * 60 * 60 * 1000);
if (!user.email_verified && new Date().getTime() > oneDayPostSignup) {
return callback(new UnauthorizedError('Please verify your email before logging in.'));
} else {
return callback(null, user, context);
}
}
To run this project you'll need a running Redis instance.
To run this project you'll need a Stripe account.
Please make sure all the items in the following checklist are marked before running this project in development:
If you're ready for production, you'll need to create the above subscription outside of the test mode too, and verify your business settings.
Run the app in dev mode (including hot module reloading) with:
npm install
npm run start-dev
Or, if you prefer using yarn, run the app with:
yarn
yarn start-dev
To run in production mode:
npm run build && npm start
Or, with yarn:
yarn build && yarn start
This project supports docker-compose.To run it, you must first:
docker-compose up -d
from the root folder of this repoIf you want to check the output of a certain container, just run docker-compose logs $CONTAINER_NAME
.$CONTAINER_NAME
may be one of the following:
redis
web
For additional infos, please check out docker-compose.yml.
This project makes an heavy use of environment variables for its configuration, so, if you want to run the project locally, you are adviced to include a .env.server
and a env.client
file in your project root (I use two dotenv files instead of one to keep the things clearer while developing).
Client environment variables: (check .env.client.example)
Environment Variable | Default | Description |
---|---|---|
REACT_APP_AUTH0_AUDIENCE |
REQUIRED | Auth0 audience |
REACT_APP_AUTH0_CLIENT_ID |
REQUIRED | Auth0 ClientID |
REACT_APP_AUTH0_DOMAIN |
REQUIRED | Auth0 domain |
REACT_APP_RATE_LIMIT_FOR_UNAUTHENTICATED_REQUESTS |
100 | Rate limit for unauthenticated users |
REACT_APP_RATE_LIMIT_FOR_FREE_USER_REQUESTS |
1000 | Rate limit for free users |
REACT_APP_RATE_LIMIT_FOR_PRO_USER_REQUESTS |
100000 | Rate limit for pro users |
REACT_APP_STRIPE_FREE_PLAN_ID |
REQUIRED | Free plan ID in Stripe |
REACT_APP_STRIPE_PRO_PLAN_ID |
REQUIRED | Pro plan ID in Stripe |
REACT_APP_STRIPE_PRO_PLAN_AMOUNT |
399 | The pro plan subscription amount in Stripe |
REACT_APP_STRIPE_PUBLIC_KEY |
REQUIRED | Stripe API public key |
REACT_APP_MAX_API_TOKENS_PER_USER |
5 | The maximum number of API tokens per user |
REACT_APP_GOOGLE_SITE_VERIFICATION |
OPTIONAL | The Google Search Console verification key |
Server environment variables: (check .env.server.example)
Environment Variable | Default | Description |
---|---|---|
PORT |
1337 | The port where the server will run |
AUTH0_AUDIENCE |
REQUIRED | Auth0 audience |
AUTH0_DOMAIN |
REQUIRED | Auth0 audience |
AUTH0_ISSUER |
REQUIRED | Auth0 audience |
AUTH0_JWKS_URI |
REQUIRED | Auth0 audience |
AUTH0_MANAGEMENT_API_AUDIENCE |
REQUIRED | Auth0 audience |
AUTH0_MANAGEMENT_API_CLIENT_ID |
REQUIRED | Auth0 audience |
AUTH0_MANAGEMENT_API_CLIENT_SECRET |
REQUIRED | Auth0 audience |
EXECUTION_ENV |
development | Used mainly for logging infos |
RATE_LIMIT_FOR_UNAUTHENTICATED_REQUESTS |
100 | Rate limit for unauthenticated users |
RATE_LIMIT_FOR_FREE_USER_REQUESTS |
1000 | Rate limit for free users |
RATE_LIMIT_FOR_PRO_USER_REQUESTS |
100000 | Rate limit for pro users |
REDIS_URL |
REQUIRED | Redis URL |
REDIS_CACHE_EXPIRY_IN_MS |
ms('1d') | Expiration of Redis cache in milliseconds |
STRIPE_FREE_PLAN_ID |
REQUIRED | Free plan ID in Stripe |
STRIPE_PRO_PLAN_ID |
REQUIRED | Pro plan ID in Stripe |
STRIPE_PUBLIC_KEY |
REQUIRED | Stripe API public key |
STRIPE_SECRET_KEY |
REQUIRED | Stripe API secret key |
PAPERTRAIL_HOST |
OPTIONAL | Papertrail URL |
PAPERTRAIL_PORT |
OPTIONAL | Papertrail port |
SENTRY_DSN |
OPTIONAL | Sentry DSN |
Pull requests are welcome. File an issue for ideas, conversation or feedback.