Web-App service
The Web App service is a Next.js application that serves as the origin server for the carbon-platform project.
Setting up local environment variables
To run the app locally certain environment variales are necessary. follow the steps below for correct setup:
- Copy the
/services/web-app/.env.example
file and rename to/services/web-app/.env.local
Adding GitHub personal access token for prototype
Prerequisite: .env.local
file for local development (see Setting up local environment variables)
The web app prototype uses a GitHub personal access token to fetch data from GitHub. This allows access to private repos and increases API quotas. To set up a personal access token:
- Generate a new token https://github.com/settings/tokens
- Select all repo scopes
- Copy the
/services/web-app/.env.example
file and rename to/services/web-app/.env.local
- Add your token to that new
.env.local
file
INDEX_ALL
Setting this env var to “1” shows all libraries and assets in the web app even if they are indexed as noIndex: true
.
Dependencies Set up
Run Mode
The app uses run-mode to make decisions that are environment-dependant (cookies, next-server…). This package configuration is also needed for proper functionality of the Auth Package.
Make sure required environment variables are configured for proper functionality of this package. (See run-mode package for details on how to set up the run-mode package)
Auth
This service depends on the auth package; Make sure required environment variables are configured for proper functionality of this package. (See auth package for details on how to set up the auth package)
Adding Local Certificates
In Standard run mode, the application must run on https for IBMid authentication to work properly. For such purposes, local certificates must be generated to use authentication in the web-app when running locally in Standard run mode. The mkcert tool can help generate these certificates and is used implicitly by the dev:secure
node script of the web-app. With the tool downloaded, the certificates will be automatically generated the next time you run the app securely.
Note: you may also choose to use IBMid authentication when running on Dev mode, in that case the same steps apply
Running on Dev Mode
To run the app, run the following command from the web-app’s directory:
npm run dev
or, alternatively, run npm run dev:clean
to run from a clean environment
App will run on http://localhost:3000
Running App Securely
To run the app on https, it must be served from proxy-server.js. To do this, run the following command from the web-app’s directory:
npm run dev:secure
The proxied version of the app will run on https://localhost:8443. You can still access the unproxied application at http://localhost:3000
Proxy Server
In Standard mode, the app uses a proxy server to perform some middleware and routing functionalities, such as logging. These configurations are stored in proxy-server.js
within the web-app folder. To run the app as close to the Standard mode as possible, see Running App Securely. If you do not wish to run the app over https, you may choose to manually run the web-app and the proxy by running the following commands in separate terminals from the web-app’s directory:
npm run dev
, npm run start:proxy
The proxied version of the app will run on http://localhost:8080. You can still access the unproxied application at http://localhost:3000
Protecting a Route
Protecting Server-Side Rendered Pages
In order to require authentication before a user can access a server-side rendered page use the getPropsWithAuth
utlity function to wrap the pages’ getServerSideProps
function and wrap the pages’ content inside the RequireAuth
component.
getPropsWithAuth
getPropsWithAuth
expects an authorizationChecker
function that receives the server side context as a param and must return a boolean value indicating whether the user is authorized to view the content or not:
import { getPropsWithAuth } from '@/utils/getPropsWithAuth'
import { retrieveUser } from '@/utils/retrieveUser'
import { isValidIbmEmail } from '@/utils/string'
// ...
// Your custom authorization logic here, this one considers the user as authorized if it's email is a valid ibm email
const isValidIbmUser = async (context) => {
const user = await retrieveUser(context)
if (user) {
return isValidIbmEmail(user.email ?? '')
}
return false
}
// ...
export const getServerSideProps = getPropsWithAuth(isValidIbmUser, async (/* context */) => {
// Your normal `getServerSideProps` code here
return {
props: {
// Your custom props here
}
}
})
Some authorizationChecker
functions that serve common purposes are exported from services/web-app/utils/auth-checkers
:
import { getPropsWithAuth } from '@/utils/getPropsWithAuth'
import isValidIbmUser from '@/utils/auth-checkers/isValidIbmUser'
// ...
export const getServerSideProps = getPropsWithAuth(isValidIbmUser, async (/* context */) => {
// Your normal `getServerSideProps` code here
return {
props: {
// Your custom props here
}
}
})
Note: if your authorizationChecker
function calls retrieveUser()
, the obtained user (if any) will be injected into the pages’s props; you can access it on props.user
RequireAuth
RequireAuth
is a parent component that receives a fallback
component which will be rendered instead of the supplied content in the case that the isAuthorized
prop is set to false
import RequireAuth from '@/components/auth/require-auth'
import FourOhFour from '@/pages/404'
// ...
const ProtectedPage = (props) => {
return (
// will return 404 page if isAuthorized is set to false
<RequireAuth fallback={FourOhFour} isAuthorized={props.isAuthorized}>
<div>User: {JSON.stringify(props?.user ?? {})}</div>
</RequireAuth>
)
}
For a working example of a protected server side rendered pages, run the app securely and visit:
- https://localhost/samples/protectedPageWithSSR
- https://localhost/samples/protectedPageWithSSRDynamicAuth
- https://localhost/samples/protectedPageWithSSRDynamicAuth?host=github.com
- https://localhost/samples/protectedPageWithSSRDynamicAuth?host=github.ibm.com
- https://localhost/samples/protectedPageWithSSRDynamicAuth?host=github.ibm.com&repo=internal-stuff
- https://localhost/samples/protectedPageWithSSRDynamicAuth?host=github.ibm.com&repo=private-stuff
Protecting Static Pages
There is no reusable strategy to guarantee authentication before accessing Statically Generated Pages; Each page is responisble for handling authentication and authorization at a component-level (e.g., fetching user , waiting for user to load and then displaying content). See “Authenticating Statically Generated Pages” section in NextJs Authentication Docs You may make use of the useAuth()
hook and RequireAuth
component in your implementations:
import { useAuth } from '@/contexts/auth'
import { useEffect } from 'react'
import RequireAuth from '@/components/auth/require-auth'
import { isValidIbmEmail } from '@/utils/string'
import FourOhFour from '../404'
const ProtectedStaticPage = () => {
const { isAuthenticated, loading, user } = useAuth()
useEffect(() => {
if (!loading && isAuthenticated) {
// fetch protected data here
}
}, [loading, isAuthenticated])
return loading ? null : (
// show page if user is authenticated and email is valid ibm email, else show 404 page
<RequireAuth
fallback={FourOhFour}
isAuthorized={isAuthenticated && isValidIbmEmail(user?.email ?? '')}
>
<>// Your Page Content Here</>
</RequireAuth>
)
}
// do NOT fetch protected data here
// at the time of build there's no logged in user available, so the server can;t perform any assertion regarding permissions when generating the pages; they're performed automatically by the server at the server's discretion.
export async function getStaticProps(/* context */) {
return {
props: {} // will be passed to the page component as props
}
}
export default ProtectedStaticPage
Note: DO NOT SERVE/FETCH PROTECTED INFO THROUGH getStaticProps
, as these will be served automatically when the route is accessed regardless of authentication.
For a working example of a protected static page, (run the app securely)[#running-app-securely] and visit https://localhost/samples/protectedStaticPage
Retrieving User’s Data
Server Side Rendered Pages
retrieveUser
The retrieveUser
function will return the current user instance or null if user is not logged in; note this function is asynchronous. This function can only be called server-side (i.e., in the context of getServerSideProps):
import { retrieveUser } from '@/utils/retrieveUser'
// ...
export const getServerSideProps = async (/* context */) => {
const user = await retrieveUser(context)
// Your normal `getServerSideProps` code here
return {
props: {
user
// Your custom props here
}
}
}
Note: if you are wrapping your getServerSideProps
with getPropsWithAuth
and your authorizationChecker
function calls retrieveUser()
, the obtained user (if any) will be injected into the pages’s props; you can access it on props.user
useAuth
From a React Component, you can access the user’s information via the user
property returned by useAuth
hook. This hook also exposes other info such as:
user
: object containing user’s dataloading
: boolean indicating whether user is still being retrieved or notisAuthenticated
: booleand indicating whether the user has correctly authenticated or notlogin
: function to login userlogout
: function to logout user
import { useAuth } from '@/contexts/auth'
// ...
const { isAuthenticated, isLoading, user, login, logout } = useAuth()
Note: Keep in mind in some instances the user might still be loading (being retrieved) or might not exist yet (user hasn’t authenticated) when your component is trying to access it. Leverage the isLoading
and isAuthenticated
properties for correct assertions
API Call
If necessary, you can make a fetch request to ‘/api/user’ which will return the user details or a 404 status code if no user has been found:
const userResponse = await fetch('/api/user')
if (userResponse.ok) {
const user = await userResponse.json()
}
expect the user response to look like this:
{
"name": "Jane Doe",
"email": "jane.doe@emaildomain.com"
// ...Other User Properties
}
Production build bundle analysis
To run the bundle analyzer against the web-app (to see where certain deps are being included which may affect initial page load times), run the following command:
(From the top-level directory)
ANALYZE=true npm -w services/web-app run build
(From the services/web-app
directory)
ANALYZE=true npm run build