React - Role Based Authorization Tutorial with Example
Tutorial built with React 16.7 and Webpack 4.29
Other versions available:
In this tutorial we'll go through an example of how you can implement role based authorization / access control using React. The example builds on another tutorial I posted recently which focuses on JWT authentication in React + Redux, in this version I've removed redux to show how you can build a react app without redux, and extended the example to include role based authorization / access control on top of the JWT authentication.
The tutorial example is pretty minimal and contains just 3 pages to demonstrate role based authorization in React - a login page, a home page and an admin page. The example contains two users - a Normal User who has access to the home page, and an Admin User who has access to everything (home page and admin page).
The project is available on GitHub at https://github.com/cornflourblue/react-role-based-authorization-example.
Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/react-role-based-authorization-example)
Running the React Role Based Authorization Example Locally
The tutorial example uses Webpack 4 to transpile the React code and bundle the modules together, and the webpack dev server is used as the local web server, to learn more about using webpack you can check out the official webpack docs.
- Install NodeJS and NPM from https://nodejs.org/en/download/.
- Download or clone the tutorial project source code from https://github.com/cornflourblue/react-role-based-authorization-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Start the application by running
npm start
from the command line in the project root folder. - Your browser should automatically open at
http://localhost:8080
with the login page of the demo React role-based authorization app displayed.
For more info on setting up a React development environment see React - Setup Development Environment.
Running the Tutorial Example with a Real Backend API
The React role based access control example app uses a fake / mock backend by default so it can run in the browser without a real api, to switch to a real backend api you just have to remove or comment out the 2 lines below the comment // setup fake backend
located in the /src/index.jsx
file.
You can build your own backend api or start with one of the below options:
- To run the React role based auth example with a real backend API built with Node.js follow the instructions at Node.js - Role Based Authorization Tutorial with Example API
- For a real backend API built with ASP.NET Core 2.2 follow the instructions at ASP.NET Core 2.2 - Role Based Authorization Tutorial with Example API
React Role Based Access Control Project Structure
All source code for the React role based authorization tutorial is located in the /src
folder. Inside the src folder there is a folder per feature (App, AdminPage, HomePage, LoginPage) and few folders for non-feature code that can be shared across different parts of the app (_components, _helpers, _services).
I prefixed non-feature folders with an underscore "_" to group them together and make it easy to distinguish between features and non-features, it also keeps the project folder structure shallow so it's quick to see everything at a glance from the top level and to navigate around the project.
The index.js
files in each folder are barrel files that group the exported modules from a folder together so they can be imported using the folder path instead of the full module path and to enable importing multiple modules in a single import (e.g. import { userService, authenticationService } from '@/_services'
).
A path alias '@' has been configured in the webpack.config.js that maps to the '/src' directory. This allows imports to be relative to the '/src' folder by prefixing the import path with '@', removing the need to use long relative paths like import MyComponent from '../../../MyComponent'
.
Click any of the below links to jump down to a description of each file along with it's code:
- src
- _components
- PrivateRoute.jsx
- index.js
- _helpers
- _services
- authentication.service.js
- user.service.js
- index.js
- App
- App.jsx
- index.js
- AdminPage
- AdminPage.jsx
- index.js
- HomePage
- HomePage.jsx
- index.js
- LoginPage
- LoginPage.jsx
- index.js
- index.html
- index.jsx
- _components
- .babelrc
- package.json
- webpack.config.js
React Tutorial Components Folder
The _components folder contains shared React components that can be used anywhere in the application.
React Tutorial Private Route Component
The react private route component renders a route component if the user is logged in and in an authorised role for the route, if the user isn't logged in they're redirected to the /login
page, if the user is logged in but aren't in an authorised role they're redirected to the home page.
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { authenticationService } from '@/_services';
export const PrivateRoute = ({ component: Component, roles, ...rest }) => (
<Route {...rest} render={props => {
const currentUser = authenticationService.currentUserValue;
if (!currentUser) {
// not logged in so redirect to login page with the return url
return <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
}
// check if route is restricted by role
if (roles && roles.indexOf(currentUser.role) === -1) {
// role not authorised so redirect to home page
return <Redirect to={{ pathname: '/'}} />
}
// authorised so return component
return <Component {...props} />
}} />
)
React Tutorial Helpers Folder
The helpers folder contains all the bits and pieces that don't fit into other folders but don't justify having a folder of their own.
React Tutorial Auth Header
Auth header is a helper function that returns an HTTP Authorization header containing the JWT auth token of the currently logged in user. If the user isn't logged in an empty object is returned.
The auth header is used to make authenticated HTTP requests to the server api using JWT authentication.
import { authenticationService } from '@/_services';
export function authHeader() {
// return authorization header with jwt token
const currentUser = authenticationService.currentUserValue;
if (currentUser && currentUser.token) {
return { Authorization: `Bearer ${currentUser.token}` };
} else {
return {};
}
}
React Tutorial Fake / Mock Backend
The fake backend enables the example to run without a backend (backend-less), it contains a hardcoded collection of users and provides fake implementations for the api endpoints "authenticate", "get user by id", and "get all users", these would be handled by a real api and database in a production application.
The "authenticate" endpoint is used for logging in to the application and is publicly accessible, the "get user by id" is restricted to authenticated users in any role, however regular users can only access their own user record whereas admin users can access any user record. The "get all users" endpoint is restricted to admin users only.
The fake backend is implemented by monkey patching the fetch()
function to intercept certain api requests and mimic the behaviour of a real api. Any requests that aren't intercepted get passed through to the real fetch()
function.
import { Role } from './'
export function configureFakeBackend() {
let users = [
{ id: 1, username: 'admin', password: 'admin', firstName: 'Admin', lastName: 'User', role: Role.Admin },
{ id: 2, username: 'user', password: 'user', firstName: 'Normal', lastName: 'User', role: Role.User }
];
let realFetch = window.fetch;
window.fetch = function (url, opts) {
const authHeader = opts.headers['Authorization'];
const isLoggedIn = authHeader && authHeader.startsWith('Bearer fake-jwt-token');
const roleString = isLoggedIn && authHeader.split('.')[1];
const role = roleString ? Role[roleString] : null;
return new Promise((resolve, reject) => {
// wrap in timeout to simulate server api call
setTimeout(() => {
// authenticate - public
if (url.endsWith('/users/authenticate') && opts.method === 'POST') {
const params = JSON.parse(opts.body);
const user = users.find(x => x.username === params.username && x.password === params.password);
if (!user) return error('Username or password is incorrect');
return ok({
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
token: `fake-jwt-token.${user.role}`
});
}
// get user by id - admin or user (user can only access their own record)
if (url.match(/\/users\/\d+$/) && opts.method === 'GET') {
if (!isLoggedIn) return unauthorised();
// get id from request url
let urlParts = url.split('/');
let id = parseInt(urlParts[urlParts.length - 1]);
// only allow normal users access to their own record
const currentUser = users.find(x => x.role === role);
if (id !== currentUser.id && role !== Role.Admin) return unauthorised();
const user = users.find(x => x.id === id);
return ok(user);
}
// get all users - admin only
if (url.endsWith('/users') && opts.method === 'GET') {
if (role !== Role.Admin) return unauthorised();
return ok(users);
}
// pass through any requests not handled above
realFetch(url, opts).then(response => resolve(response));
// private helper functions
function ok(body) {
resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) })
}
function unauthorised() {
resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorised' })) })
}
function error(message) {
resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) })
}
}, 500);
});
}
}
React Tutorial Handle Response
The handleResponse function checks responses from the api to see if the request was unauthorised, forbidden or unsuccessful.
If the response status is 401 Unauthorized
or 403 Forbidden
then the user is automatically logged out of the application, this handles if the user token is no longer valid for any reason. If the response contains an error then a rejected promise is returned that includes the error message, otherwise if the request was successful then the response data is returned as a JSON object.
import { authenticationService } from '@/_services';
export function handleResponse(response) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if ([401, 403].indexOf(response.status) !== -1) {
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
authenticationService.logout();
location.reload(true);
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}
React Tutorial History
The history is a custom history object used by the React Router, the reason I used a custom history object instead of the one built into React Router is to enable redirecting users from outside React components, for example in the logout method of the App component.
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();
React Tutorial Role Object / Enum
The role object defines the all the roles in the example application, I created it to use like an enum to avoid passing roles around as strings, so instead of 'Admin'
we can use Role.Admin
.
export const Role = {
Admin: 'Admin',
User: 'User'
}
React Tutorial Services Folder
The _services layer handles all http communication with backend apis for the application, each service encapsulates the api calls for a content type (e.g. users) and exposes methods for performing various operations (e.g. CRUD operations). Services can also have methods that don't wrap http calls, for example the authenticationService.logout()
method just removes the currentUser object from localStorage and sets it to null in the application.
I like wrapping http calls and implementation details in a services layer because it provides a clean separation of concerns and simplifies the react components that use the services.
React Tutorial Authentication Service
The authentication service is used to login and logout of the application, to login it posts the user's credentials to the /users/authenticate
route on the api, if authentication is successful the user details including the token are added to local storage, and the current user is set in the application by calling currentUserSubject.next(user);
.
The logged in user details are stored in local storage so the user will stay logged in if they refresh the browser and also between browser sessions until they explicitly logout. If you don't want the user to stay logged in between refreshes or sessions the behaviour could easily be changed by storing user details somewhere less persistent such as session storage which would persist between refreshes but not browser sessions, or you could remove the calls to localStorage which would cause the user to be logged out if the browser is refreshed.
There are two properties exposed by the authentication service for accessing the currently logged in user. The currentUser
observable can be used when you want a component to reactively update when a user logs in or out, for example in the App.jsx component so it can show/hide the main nav bar when the user logs in/out. The currentUserValue
property can be used when you just want to get the current value of the logged in user but don't need to reactively update when it changes, for example in the PrivateRoute.jsx component which restricts access to routes by checking if the user is currently logged in and authorised.
import { BehaviorSubject } from 'rxjs';
import config from 'config';
import { handleResponse } from '@/_helpers';
const currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser')));
export const authenticationService = {
login,
logout,
currentUser: currentUserSubject.asObservable(),
get currentUserValue () { return currentUserSubject.value }
};
function login(username, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
};
return fetch(`${config.apiUrl}/users/authenticate`, requestOptions)
.then(handleResponse)
.then(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify(user));
currentUserSubject.next(user);
return user;
});
}
function logout() {
// remove user from local storage to log user out
localStorage.removeItem('currentUser');
currentUserSubject.next(null);
}
React Tutorial User Service
The user service contains just a couple of methods for retrieving user data from the api, it acts as the interface between the React application and the backend api.
I included the user service to demonstrate accessing secure api endpoints with the http authorization header set after logging in to the application, the auth header is set with a JWT token in the auth-header.js helper above. The secure endpoints in the example are fake/mock routes implemented in the fake-backend.js helper above.
import config from 'config';
import { authHeader, handleResponse } from '@/_helpers';
export const userService = {
getAll,
getById
};
function getAll() {
const requestOptions = { method: 'GET', headers: authHeader() };
return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse);
}
function getById(id) {
const requestOptions = { method: 'GET', headers: authHeader() };
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
}
React Tutorial App Folder
The app folder is for react components and other code that is used only by the app component in the tutorial application.
React Tutorial App Component
The app component is the root component for the react tutorial application, it contains the outer html, routes and main nav bar for the example app.
It subscribes to the currentUser
observable in the authentication service so it can reactively show/hide the main navigation bar when the user logs in/out of the application. I didn't worry about unsubscribing from the observable here because it's the root component of the application, so the only time the component will be destroyed is when the application is closed which would destroy any subscriptions as well.
The app component contains a logout()
method which is called from the logout link in the main nav bar to log the user out and redirect them to the login page.
import React from 'react';
import { Router, Route, Link } from 'react-router-dom';
import { history, Role } from '@/_helpers';
import { authenticationService } from '@/_services';
import { PrivateRoute } from '@/_components';
import { HomePage } from '@/HomePage';
import { AdminPage } from '@/AdminPage';
import { LoginPage } from '@/LoginPage';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currentUser: null,
isAdmin: false
};
}
componentDidMount() {
authenticationService.currentUser.subscribe(x => this.setState({
currentUser: x,
isAdmin: x && x.role === Role.Admin
}));
}
logout() {
authenticationService.logout();
history.push('/login');
}
render() {
const { currentUser, isAdmin } = this.state;
return (
<Router history={history}>
<div>
{currentUser &&
<nav className="navbar navbar-expand navbar-dark bg-dark">
<div className="navbar-nav">
<Link to="/" className="nav-item nav-link">Home</Link>
{isAdmin && <Link to="/admin" className="nav-item nav-link">Admin</Link>}
<a onClick={this.logout} className="nav-item nav-link">Logout</a>
</div>
</nav>
}
<div className="jumbotron">
<div className="container">
<div className="row">
<div className="col-md-6 offset-md-3">
<PrivateRoute exact path="/" component={HomePage} />
<PrivateRoute path="/admin" roles={[Role.Admin]} component={AdminPage} />
<Route path="/login" component={LoginPage} />
</div>
</div>
</div>
</div>
</div>
</Router>
);
}
}
export { App };
React Tutorial Admin Page Folder
The admin page folder is for react components and other code that is used only by the admin page component in the tutorial application.
React Tutorial Admin Page Component
The admin page calls the user service to get all users from a secure api endpoint and displays them in a html list. The page is restricted to users in the 'Admin' role.
import React from 'react';
import { userService } from '@/_services';
class AdminPage extends React.Component {
constructor(props) {
super(props);
this.state = {
users: null
};
}
componentDidMount() {
userService.getAll().then(users => this.setState({ users }));
}
render() {
const { users } = this.state;
return (
<div>
<h1>Admin</h1>
<p>This page can only be accessed by administrators.</p>
<div>
All users from secure (admin only) api end point:
{users &&
<ul>
{users.map(user =>
<li key={user.id}>{user.firstName} {user.lastName}</li>
)}
</ul>
}
</div>
</div>
);
}
}
export { AdminPage };
React Tutorial Home Page Folder
The home page folder is for react components and other code that is used only by the home page component in the tutorial application.
React Tutorial Home Page Component
The home page component is displayed after signing in to the application, it contains a simple welcome message and the current user record. The component gets the current user from the authentication service and then fetches the current user from the api by calling the userService.getById(currentUser.id)
method from the componentDidMount()
react lifecycle hook.
We only really needed to get the user from the authentication service, but I included getting it from the user service as well to demonstrate fetching data from a secure api endpoint.
import React from 'react';
import { userService, authenticationService } from '@/_services';
class HomePage extends React.Component {
constructor(props) {
super(props);
this.state = {
currentUser: authenticationService.currentUserValue,
userFromApi: null
};
}
componentDidMount() {
const { currentUser } = this.state;
userService.getById(currentUser.id).then(userFromApi => this.setState({ userFromApi }));
}
render() {
const { currentUser, userFromApi } = this.state;
return (
<div>
<h1>Home</h1>
<p>You're logged in with React & JWT!!</p>
<p>Your role is: <strong>{currentUser.role}</strong>.</p>
<p>This page can be accessed by all authenticated users.</p>
<div>
Current user from secure api end point:
{userFromApi &&
<ul>
<li>{userFromApi.firstName} {userFromApi.lastName}</li>
</ul>
}
</div>
</div>
);
}
}
export { HomePage };
React Tutorial Login Page Folder
The login page folder is for react components and other code that is used only by the login page component in the tutorial application.
React Tutorial Login Page Component
The login page component contains a login form with username and password fields. It displays validation messages for invalid fields when the user attempts to submit the form or when a field is touched. If the form is valid the component calls the authenticationService.login(username, password)
method, if login is successful the user is redirected back to the original page they were trying to access.
There's an info alert message above the form with the login details for two example users, a normal user in the User
role and an admin user in the Admin
role.
I built the login form using Formik, a higher order component that helps with managing form state, validation, error messages, and form submission. Validation is done with the Yup object schema validator which hooks into Formik via the handy validationSchema
prop.
import React from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { authenticationService } from '@/_services';
class LoginPage extends React.Component {
constructor(props) {
super(props);
// redirect to home if already logged in
if (authenticationService.currentUserValue) {
this.props.history.push('/');
}
}
render() {
return (
<div>
<div className="alert alert-info">
<strong>Normal User</strong> - U: user P: user<br />
<strong>Administrator</strong> - U: admin P: admin
</div>
<h2>Login</h2>
<Formik
initialValues={{
username: '',
password: ''
}}
validationSchema={Yup.object().shape({
username: Yup.string().required('Username is required'),
password: Yup.string().required('Password is required')
})}
onSubmit={({ username, password }, { setStatus, setSubmitting }) => {
setStatus();
authenticationService.login(username, password)
.then(
user => {
const { from } = this.props.location.state || { from: { pathname: "/" } };
this.props.history.push(from);
},
error => {
setSubmitting(false);
setStatus(error);
}
);
}}
render={({ errors, status, touched, isSubmitting }) => (
<Form>
<div className="form-group">
<label htmlFor="username">Username</label>
<Field name="username" type="text" className={'form-control' + (errors.username && touched.username ? ' is-invalid' : '')} />
<ErrorMessage name="username" component="div" className="invalid-feedback" />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
<ErrorMessage name="password" component="div" className="invalid-feedback" />
</div>
<div className="form-group">
<button type="submit" className="btn btn-primary" disabled={isSubmitting}>Login</button>
{isSubmitting &&
<img src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
}
</div>
{status &&
<div className={'alert alert-danger'}>{status}</div>
}
</Form>
)}
/>
</div>
)
}
}
export { LoginPage };
React Tutorial Index HTML File
The base index html file contains the outer html for the whole tutorial application. When the app is started with npm start
, Webpack bundles up all of the react code into a single javascript file and injects it into the body of the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React - Role Based Authorization Tutorial & Example</title>
<!-- bootstrap css -->
<link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" />
<style>
a { cursor: pointer; }
</style>
</head>
<body>
<div id="app"></div>
</body>
</html>
React Tutorial Main Entry File
The root index.jsx file bootstraps the react tutorial application by rendering the App
component into the app
div element defined in the base index html file above.
The boilerplate application uses a fake / mock backend by default, to switch to a real backend api simply remove the fake backend code below the comment // setup fake backend
.
import React from 'react';
import { render } from 'react-dom';
import { App } from './App';
// setup fake backend
import { configureFakeBackend } from './_helpers';
configureFakeBackend();
render(
<App />,
document.getElementById('app')
);
React Tutorial Babel RC (Run Commands)
The babel config file defines the presets used by babel to transpile the React and ES6 code. The babel transpiler is run by webpack via the babel-loader
module configured in the webpack.config.js file below.
{
"presets": [
"react",
"env",
"stage-0"
]
}
React Tutorial Package.json
The package.json file contains project configuration information including package dependencies which get installed when you run npm install
. Full documentation is available on the npm docs website.
{
"name": "react-role-based-authorization-example",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "https://github.com/cornflourblue/react-role-based-authorization-example.git"
},
"license": "MIT",
"scripts": {
"start": "webpack-dev-server --open"
},
"dependencies": {
"formik": "^1.4.2",
"history": "^4.7.2",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-router-dom": "^4.1.2",
"rxjs": "^6.3.3",
"yup": "^0.26.10"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.24.1",
"html-webpack-plugin": "^3.2.0",
"path": "^0.12.7",
"webpack": "^4.15.0",
"webpack-cli": "^3.0.8",
"webpack-dev-server": "^3.1.3"
}
}
React Tutorial Webpack Config
Webpack is used to compile and bundle all the project files so they're ready to be loaded into a browser, it does this with the help of loaders and plugins that are configured in the webpack.config.js file. For more info about webpack check out the webpack docs.
The webpack config file also defines a global config object for the application using the externals
property, you can also use this to define different config variables for your development and production environments.
var HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader'
}
]
},
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src/'),
}
},
plugins: [new HtmlWebpackPlugin({
template: './src/index.html'
})],
devServer: {
historyApiFallback: true
},
externals: {
// global app config object
config: JSON.stringify({
apiUrl: 'http://localhost:4000'
})
}
}
Need Some React Help?
Search fiverr for freelance React developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!