From 66370d8cb5be4275e11ef76641655e20140be9de Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Tue, 29 Mar 2022 13:18:41 +0100
Subject: [PATCH] Handle storage events, handle more OIDC errors, developer
 portal start

---
 ...210000001-create-session-boot-time-view.js |  23 ++
 scripts/check_img.js                          |   1 +
 scripts/populate_roots.js                     |   5 +-
 src/app.js                                    |  11 +
 src/core/services/serialise.js                |   1 -
 src/core/utils/queue.js                       |   4 +
 src/core/utils/urls.js                        |   8 +-
 src/domain/auth/AuthServer.js                 |   3 -
 src/domain/auth/AuthenticationService.js      |   6 +-
 .../uploads/handlers/MarkUploadSuccessful.js  |  27 ++
 .../controllers/api/v2/classifications.js     |   2 +-
 src/http/controllers/api/v2/uploads.js        |  41 +++
 src/http/controllers/dev/clients.js           |   3 +
 src/http/controllers/oidc.js                  |  13 +-
 src/http/params/survey.js                     |   5 -
 src/http/routers/routes_v2.js                 |   2 +
 src/http/routes.js                            |   3 +
 views/developers/index.hbs                    | 345 ++++++++++++++++++
 18 files changed, 480 insertions(+), 23 deletions(-)
 create mode 100644 database/migrations/20211210000001-create-session-boot-time-view.js
 create mode 100644 src/domain/uploads/handlers/MarkUploadSuccessful.js
 create mode 100644 src/http/controllers/dev/clients.js
 create mode 100644 views/developers/index.hbs

diff --git a/database/migrations/20211210000001-create-session-boot-time-view.js b/database/migrations/20211210000001-create-session-boot-time-view.js
new file mode 100644
index 0000000..33cb452
--- /dev/null
+++ b/database/migrations/20211210000001-create-session-boot-time-view.js
@@ -0,0 +1,23 @@
+module.exports = {
+	up: (migration, Types) => {
+		return migration.sequelize.query(`
+CREATE OR REPLACE VIEW session_boot_time as SELECT session_id,
+       max(start_time) - min(start_time) as boot_time,
+       device->>'id' as device_id,
+       concat_ws(' ', device -> 'info' ->> 'brand', device -> 'info' ->> 'system',
+                 device -> 'info' ->> 'system_version') as device_info
+FROM analytics
+where event = 'app_open'
+   or event = 'navigation_ready'
+group by session_id,
+         device->>'id',
+         concat_ws(' ', device -> 'info' ->> 'brand', device -> 'info' ->> 'system',
+                   device -> 'info' ->> 'system_version')
+having max(start_time) - min(start_time) > interval '0 seconds'
+;`)
+	},
+
+	down: (migration, Types) => {
+		return migration.sequelize.query('DROP VIEW IF EXISTS session_boot_time')
+	},
+}
\ No newline at end of file
diff --git a/scripts/check_img.js b/scripts/check_img.js
index b0f7420..8225d86 100644
--- a/scripts/check_img.js
+++ b/scripts/check_img.js
@@ -23,6 +23,7 @@ async function main() {
 			try {
 				await fs.makePublic(path)
 			} catch(e) {
+				console.error(e)
 				console.error('Couldnt make ', path, ' public')
 				upload.status = 'failed'
 				upload.status_reason = 'bogus upload url'
diff --git a/scripts/populate_roots.js b/scripts/populate_roots.js
index 4a39d21..58babb8 100644
--- a/scripts/populate_roots.js
+++ b/scripts/populate_roots.js
@@ -13,12 +13,13 @@ where
       from classification_roots 
       where classification_roots.metric_id = metrics.id
   )
-limit 20;`
+limit 100`
 
 const InsertQuery = `insert into
   classification_roots(metric_id, upload_id, image_id, url)
   ${ SelectQuery }
-`
+  ON CONFLICT DO NOTHING
+;`
 
 async function main() {
 	const { sequelize } = require('database/models')
diff --git a/src/app.js b/src/app.js
index 98f4a76..7304126 100644
--- a/src/app.js
+++ b/src/app.js
@@ -36,6 +36,17 @@ module.exports = async function createApp(app = new Koa()) {
 	app.use(logger(s => requestLog(s)))
 	app.use(static(pathutil.resolve(__dirname + '/../public')))
 
+	app.use(async (ctx, next) => {
+		if (ctx.method === 'OPTIONS') {
+			ctx.response.set('Access-Control-Allow-Origin', ctx.request.origin)
+			ctx.response.set('Access-Control-Allow-Method', ctx.request.get('Access-Control-Request-Method'))
+			ctx.response.set('Access-Control-Allow-Headers', ctx.request.get('Access-Control-Request-Headers'))
+			ctx.response.status = 201
+		} else {
+			return await next()
+		}
+	})
+
 	app.use(
 		session(
 			{
diff --git a/src/core/services/serialise.js b/src/core/services/serialise.js
index 3e80e4d..172ecbe 100644
--- a/src/core/services/serialise.js
+++ b/src/core/services/serialise.js
@@ -6,7 +6,6 @@ exports.serialise = async function serialise(pattern, data) {
 			case 'string':
 			case 'number':
 			case 'symbol':
-				console.log(data[mapper])
 				output[key] = data[mapper]
 				break
 			case 'function':
diff --git a/src/core/utils/queue.js b/src/core/utils/queue.js
index c96201a..2615461 100644
--- a/src/core/utils/queue.js
+++ b/src/core/utils/queue.js
@@ -3,6 +3,10 @@ const HANDLERS = [
 		'send-user-password-reset',
 		require('domain/auth/handlers/SendUserPasswordReset'),
 	],
+	[
+		'upload-successful',
+		require('domain/uploads/handlers/MarkUploadSuccessful'),
+	],
 ]
 
 module.exports = function bindJobHandlers() {
diff --git a/src/core/utils/urls.js b/src/core/utils/urls.js
index 2f04912..70f6934 100644
--- a/src/core/utils/urls.js
+++ b/src/core/utils/urls.js
@@ -30,7 +30,7 @@ const urlToName = redirectPairs.reduce((cur, [k, v]) => ({ ...cur, [v]: k }), {}
 
 exports.createRedirectState = async ctx => {
 	const path = ctx.path
-	const redirect = urlToName[path] ?? '/'
+	const redirect = urlToName[path] ?? `$$${ctx.path}`
 	const query = ctx.request.query ?? {}
 	return crypto.encrypt(JSON.stringify({ redirect, query }))
 }
@@ -50,7 +50,9 @@ exports.parseRedirectState = async ctx => {
 		const value = JSON.parse(raw)
 		return {
 			...value,
-			path: nameToUrl[value.redirect] ?? '/',
+			path: value.redirect.startsWith('$$') ?
+				value.redirect.substr(2) :
+				nameToUrl[value.redirect] ?? '/',
 		}
 	} catch(e) {
 		console.error(e)
@@ -68,8 +70,6 @@ exports.createRedirectedUrl = async (ctx) => {
 	const values = await exports.parseRedirectState(ctx)
 	const params = new URLSearchParams()
 
-	console.log(values, params)
-
 	Object.entries(values.query).forEach(([key, value]) => {
 		params.set(key, value)
 	})
diff --git a/src/domain/auth/AuthServer.js b/src/domain/auth/AuthServer.js
index fed5552..728af4e 100644
--- a/src/domain/auth/AuthServer.js
+++ b/src/domain/auth/AuthServer.js
@@ -223,9 +223,6 @@ class KoaOAuthServer {
 		this.token = async ctx => {
 			const { req, res } = this.transformContext(ctx)
 
-			console.log(ctx.request.query)
-			console.log(ctx.request.body)
-
 			await authServer.token(req, res, {
 				allowExtendedTokenAttributes: true,
 				accessTokenLifetime: 3600 * 24 * 7,
diff --git a/src/domain/auth/AuthenticationService.js b/src/domain/auth/AuthenticationService.js
index 78a5b5c..9da4baa 100644
--- a/src/domain/auth/AuthenticationService.js
+++ b/src/domain/auth/AuthenticationService.js
@@ -58,11 +58,15 @@ module.exports = class AuthenticationService extends ContextualModule {
 		} else if (this.ctx.get('Authorization')) {
 			const token = this.ctx.get('Authorization').substr(HEADER_PREFIX.length)
 
-			const accessToken = await AccessToken.findOne({
+			let accessToken = await AccessToken.findOne({
 				where: { token },
 				include: [{ model: User }],
 			})
 
+			if (accessToken == null) {
+				accessToken =await this.ctx.services['auth.oidc'].withProvider(provider => provider.AccessToken.find(token))
+			}
+
 			if (accessToken.User) {
 				this.authenticateAs(accessToken.User)
 				return this._user
diff --git a/src/domain/uploads/handlers/MarkUploadSuccessful.js b/src/domain/uploads/handlers/MarkUploadSuccessful.js
new file mode 100644
index 0000000..f10a57e
--- /dev/null
+++ b/src/domain/uploads/handlers/MarkUploadSuccessful.js
@@ -0,0 +1,27 @@
+
+module.exports = async (body, ctx) => {
+	const {bucket, key} = body
+	const stub = `${bucket}/${key}`
+
+	const {Upload, Sequelize} = require('database/models')
+	const upload = await Upload.findOne({
+		where: {
+			upload_url: {
+				[Sequelize.Op.like]: `%${stub}%`,
+			}
+		}
+	})
+
+	if (upload) {
+		upload.status = 'success'
+
+		try {
+			const { fs } = require('services')
+			await fs.makePublic(key)
+		} catch (e) {
+			upload.status_reason = 'Failed to make public'
+		}
+
+		await upload.save()
+	}
+}
\ No newline at end of file
diff --git a/src/http/controllers/api/v2/classifications.js b/src/http/controllers/api/v2/classifications.js
index 00f5d55..163ecec 100644
--- a/src/http/controllers/api/v2/classifications.js
+++ b/src/http/controllers/api/v2/classifications.js
@@ -14,7 +14,7 @@ exports.listRoots = async ctx => {
 		.filter(s => publicStatus.has(s) || privateStatus.has(s))
 
 	const query = {
-		order: ['metric_id', 'created_at'],
+		order: ['metric_id', ['created_at', 'DESC']],
 		where: {
 			status: {
 				[Sequelize.Op.in]: requestedStatus,
diff --git a/src/http/controllers/api/v2/uploads.js b/src/http/controllers/api/v2/uploads.js
index 62f0358..af11876 100644
--- a/src/http/controllers/api/v2/uploads.js
+++ b/src/http/controllers/api/v2/uploads.js
@@ -1,3 +1,4 @@
+const {queue} = require("services");
 exports.createUpload = async ctx => {
 	const { fs } = require('services')
 	const { v4: uuid } = require('uuid')
@@ -28,3 +29,43 @@ exports.createUpload = async ctx => {
 
 	ctx.body = { upload: payload }
 }
+
+/**
+ * {
+ *   message: {
+ *     attributes: {
+ *       bucketId: 'jetsam-uploads-staging',
+ *       eventTime: '2022-02-23T18:10:22.885464Z',
+ *       eventType: 'OBJECT_FINALIZE',
+ *       notificationConfig: 'projects/_/buckets/jetsam-uploads-staging/notificationConfigs/3',
+ *       objectGeneration: '1645639822790386',
+ *       objectId: 'funi.png',
+ *       overwroteGeneration: '1645638845013021',
+ *       payloadFormat: 'JSON_API_V1'
+ *     },
+ *     data: 'ewogICJraW5kIjogInN0b3JhZ2Ujb2JqZWN0IiwKICAiaWQiOiAiamV0c2FtLXVwbG9hZHMtc3RhZ2luZy9mdW5pLnBuZy8xNjQ1NjM5ODIyNzkwMzg2IiwKICAic2VsZkxpbmsiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vc3RvcmFnZS92MS9iL2pldHNhbS11cGxvYWRzLXN0YWdpbmcvby9mdW5pLnBuZyIsCiAgIm5hbWUiOiAiZnVuaS5wbmciLAogICJidWNrZXQiOiAiamV0c2FtLXVwbG9hZHMtc3RhZ2luZyIsCiAgImdlbmVyYXRpb24iOiAiMTY0NTYzOTgyMjc5MDM4NiIsCiAgIm1ldGFnZW5lcmF0aW9uIjogIjEiLAogICJjb250ZW50VHlwZSI6ICJpbWFnZS9wbmciLAogICJ0aW1lQ3JlYXRlZCI6ICIyMDIyLTAyLTIzVDE4OjEwOjIyLjg4NVoiLAogICJ1cGRhdGVkIjogIjIwMjItMDItMjNUMTg6MTA6MjIuODg1WiIsCiAgInN0b3JhZ2VDbGFzcyI6ICJTVEFOREFSRCIsCiAgInRpbWVTdG9yYWdlQ2xhc3NVcGRhdGVkIjogIjIwMjItMDItMjNUMTg6MTA6MjIuODg1WiIsCiAgInNpemUiOiAiMTI2MDkiLAogICJtZDVIYXNoIjogIlZhakdKT3Q0QlV4OWxubVF4YURQaUE9PSIsCiAgIm1lZGlhTGluayI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9kb3dubG9hZC9zdG9yYWdlL3YxL2IvamV0c2FtLXVwbG9hZHMtc3RhZ2luZy9vL2Z1bmkucG5nP2dlbmVyYXRpb249MTY0NTYzOTgyMjc5MDM4NiZhbHQ9bWVkaWEiLAogICJjcmMzMmMiOiAiZUZ1L0VBPT0iLAogICJldGFnIjogIkNQS2R3NGkybHZZQ0VBRT0iCn0K',
+ *     messageId: '4057500370063368',
+ *     message_id: '4057500370063368',
+ *     publishTime: '2022-02-23T18:10:23.229Z',
+ *     publish_time: '2022-02-23T18:10:23.229Z'
+ *   },
+ *   subscription: 'projects/hot-trash-server/subscriptions/jetsam-uploads-staging-events-subscription'
+ * }
+ */
+
+exports.storageEventsWebhook = async ctx => {
+	const { queue } = require('services')
+
+	const { message } = ctx.request.body
+	const bucket = message?.attributes?.bucketId
+	const key = message?.attributes?.objectId
+
+	if (message?.attributes?.eventType === 'OBJECT_FINALIZE') {
+		await queue.dispatch('upload-successful', {
+			bucket,
+			key,
+		})
+	}
+
+	ctx.status = 204
+}
\ No newline at end of file
diff --git a/src/http/controllers/dev/clients.js b/src/http/controllers/dev/clients.js
new file mode 100644
index 0000000..f0e8611
--- /dev/null
+++ b/src/http/controllers/dev/clients.js
@@ -0,0 +1,3 @@
+exports.showClients = async ctx => {
+	return await ctx.render('developers/index')
+}
\ No newline at end of file
diff --git a/src/http/controllers/oidc.js b/src/http/controllers/oidc.js
index d8d6a97..4e9acfb 100644
--- a/src/http/controllers/oidc.js
+++ b/src/http/controllers/oidc.js
@@ -20,12 +20,13 @@ exports.handleLogin = async ctx => {
 	}
 
 	const { email, password } = ctx.request.body
-	const user = await ctx.services['core.auth'].attemptLogin(email, password)
-
-	const result = {
-		login: {
-			accountId: user.id,
-		},
+	const result = {}
+	try {
+		const user = await ctx.services['core.auth'].attemptLogin(email, password)
+		result.login = { accountId: user.id }
+	} catch(e) {
+		result.error = 'invalid_credentials'
+		result.error_description = e.message
 	}
 
 	return await ctx.services['auth.oidc'].withProvider(p => p.interactionFinished(ctx.req, ctx.res, result, {
diff --git a/src/http/params/survey.js b/src/http/params/survey.js
index 92ded5d..70af8a6 100644
--- a/src/http/params/survey.js
+++ b/src/http/params/survey.js
@@ -14,9 +14,6 @@ module.exports = async (id, ctx, next) => {
 		}
 	}] : []
 
-	console.log("INCLUDES", includes)
-
-
 	ctx.models.survey = await Survey.findOne({
 		where: {
 			id,
@@ -31,7 +28,5 @@ module.exports = async (id, ctx, next) => {
 		include: includes
 	})
 
-	console.log(ctx.models.survey)
-
 	return await next()
 }
diff --git a/src/http/routers/routes_v2.js b/src/http/routers/routes_v2.js
index 3a1e040..7c4683d 100644
--- a/src/http/routers/routes_v2.js
+++ b/src/http/routers/routes_v2.js
@@ -69,6 +69,8 @@ if (config('app.dev')) {
 router.get('/classifications/roots', controller('api/v2/classifications', 'listRoots'))
 router.put('/classifications/roots/:classification/status', controller('api/v2/classifications', 'putRootStatus'))
 
+router.post('/webhooks/storage/events', controller('api/v2/uploads', 'storageEventsWebhook'))
+
 router.post('/an/ev', safemode, controller('api/analytics', 'track'))
 
 module.exports = router
diff --git a/src/http/routes.js b/src/http/routes.js
index c948e3b..f206ce0 100644
--- a/src/http/routes.js
+++ b/src/http/routes.js
@@ -63,6 +63,9 @@ web.post('/reset-password', controller('auth', 'handleResetPassword'))
 web.get('/auth/authorize', authRedirect, AuthServer.authorize)
 web.post('/auth/authorize', AuthServer.authorize)
 web.post('/auth/token', AuthServer.token)
+
+web.get('/developers', authRedirect, controller('dev/clients', 'showClients'))
+
 env('FS_DRIVER', 'local') === 'local' &&
 	(function () {
 		const debug = require('debug')('server:routes')
diff --git a/views/developers/index.hbs b/views/developers/index.hbs
new file mode 100644
index 0000000..6a00a70
--- /dev/null
+++ b/views/developers/index.hbs
@@ -0,0 +1,345 @@
+<!DOCTYPE html>
+
+<html class="h-full bg-gray-100" lang="en">
+<head>
+	<title>Login | Jetsam</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+	<meta name="description" content="">
+	<meta name="author" content="">
+	<!--	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">-->
+	<script src="https://cdn.tailwindcss.com"></script>
+
+	<style>
+		body, body > div {
+			height: 100%;
+		}
+	</style>
+</head>
+<body class="h-full">
+<aside class="lg:grid lg:grid-cols-12 lg:gap-x-5">
+	<div class="flex-1 flex flex-col min-h-0 bg-indigo-700">
+		<div class="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
+			<div class="flex items-center flex-shrink-0 px-4">
+				<img class="h-8 w-auto" src="https://tailwindui.com/img/logos/workflow-logo-indigo-300-mark-white-text.svg" alt="Workflow">
+			</div>
+			<nav class="mt-5 flex-1 px-2 space-y-1">
+				<!-- Current: "bg-indigo-800 text-white", Default: "text-white hover:bg-indigo-600 hover:bg-opacity-75" -->
+				<a href="#" class="bg-indigo-800 text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md">
+					<!-- Heroicon name: outline/home -->
+					<svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
+						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
+					</svg>
+					Dashboard
+				</a>
+
+				<a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md">
+					<!-- Heroicon name: outline/users -->
+					<svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
+						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
+					</svg>
+					Team
+				</a>
+
+				<a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md">
+					<!-- Heroicon name: outline/folder -->
+					<svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
+						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
+					</svg>
+					Projects
+				</a>
+
+				<a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md">
+					<!-- Heroicon name: outline/calendar -->
+					<svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
+						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
+					</svg>
+					Calendar
+				</a>
+
+				<a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md">
+					<!-- Heroicon name: outline/inbox -->
+					<svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
+						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
+					</svg>
+					Documents
+				</a>
+
+				<a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md">
+					<!-- Heroicon name: outline/chart-bar -->
+					<svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
+						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+					</svg>
+					Reports
+				</a>
+			</nav>
+		</div>
+		<div class="flex-shrink-0 flex border-t border-indigo-800 p-4">
+			<a href="#" class="flex-shrink-0 w-full group block">
+				<div class="flex items-center">
+					<div>
+						<img class="inline-block h-9 w-9 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
+					</div>
+					<div class="ml-3">
+						<p class="text-sm font-medium text-white">Tom Cook</p>
+						<p class="text-xs font-medium text-indigo-200 group-hover:text-white">View profile</p>
+					</div>
+				</div>
+			</a>
+		</div>
+	</div>
+</aside>
+<!--<div class="md:pl-64 flex flex-col flex-1">-->
+<!--	<div class="sticky top-0 z-10 md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3 bg-gray-100">-->
+<!--		<button type="button" class="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">-->
+<!--			<span class="sr-only">Open sidebar</span>-->
+<!--			&lt;!&ndash; Heroicon name: outline/menu &ndash;&gt;-->
+<!--			<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">-->
+<!--				<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />-->
+<!--			</svg>-->
+<!--		</button>-->
+<!--	</div>-->
+
+	<div class="flex-1 space-y-6 sm:px-6 lg:px-0 lg:col-span-9">
+		<form action="#" method="POST">
+			<div class="shadow sm:rounded-md sm:overflow-hidden">
+				<div class="bg-white py-6 px-4 space-y-6 sm:p-6">
+					<div>
+						<h3 class="text-lg leading-6 font-medium text-gray-900">Profile</h3>
+						<p class="mt-1 text-sm text-gray-500">This information will be displayed publicly so be careful
+							what you share.</p>
+					</div>
+
+					<div class="grid grid-cols-3 gap-6">
+						<div class="col-span-3 sm:col-span-2">
+							<label for="company-website" class="block text-sm font-medium text-gray-700">
+								Username </label>
+							<div class="mt-1 rounded-md shadow-sm flex">
+								<span class="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm"> workcation.com/ </span>
+								<input type="text" name="username" id="username" autocomplete="username"
+									   class="focus:ring-indigo-500 focus:border-indigo-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300">
+							</div>
+						</div>
+
+						<div class="col-span-3">
+							<label for="about" class="block text-sm font-medium text-gray-700"> About </label>
+							<div class="mt-1">
+								<textarea id="about" name="about" rows="3"
+										  class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
+										  placeholder="you@example.com"></textarea>
+							</div>
+							<p class="mt-2 text-sm text-gray-500">Brief description for your profile. URLs are
+								hyperlinked.</p>
+						</div>
+
+						<div class="col-span-3">
+							<label class="block text-sm font-medium text-gray-700"> Photo </label>
+							<div class="mt-1 flex items-center">
+                <span class="inline-block bg-gray-100 rounded-full overflow-hidden h-12 w-12">
+                  <svg class="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
+                    <path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"/>
+                  </svg>
+                </span>
+								<button type="button"
+										class="ml-5 bg-white border border-gray-300 rounded-md shadow-sm py-2 px-3 text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+									Change
+								</button>
+							</div>
+						</div>
+
+						<div class="col-span-3">
+							<label class="block text-sm font-medium text-gray-700"> Cover photo </label>
+							<div class="mt-1 border-2 border-gray-300 border-dashed rounded-md px-6 pt-5 pb-6 flex justify-center">
+								<div class="space-y-1 text-center">
+									<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none"
+										 viewBox="0 0 48 48" aria-hidden="true">
+										<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
+											  stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+									</svg>
+									<div class="flex text-sm text-gray-600">
+										<label for="file-upload"
+											   class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
+											<span>Upload a file</span>
+											<input id="file-upload" name="file-upload" type="file" class="sr-only">
+										</label>
+										<p class="pl-1">or drag and drop</p>
+									</div>
+									<p class="text-xs text-gray-500">PNG, JPG, GIF up to 10MB</p>
+								</div>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
+					<button type="submit"
+							class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+						Save
+					</button>
+				</div>
+			</div>
+		</form>
+
+		<form action="#" method="POST">
+			<div class="shadow sm:rounded-md sm:overflow-hidden">
+				<div class="bg-white py-6 px-4 space-y-6 sm:p-6">
+					<div>
+						<h3 class="text-lg leading-6 font-medium text-gray-900">Personal Information</h3>
+						<p class="mt-1 text-sm text-gray-500">Use a permanent address where you can recieve mail.</p>
+					</div>
+
+					<div class="grid grid-cols-6 gap-6">
+						<div class="col-span-6 sm:col-span-3">
+							<label for="first-name" class="block text-sm font-medium text-gray-700">First name</label>
+							<input type="text" name="first-name" id="first-name" autocomplete="given-name"
+								   class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+						</div>
+
+						<div class="col-span-6 sm:col-span-3">
+							<label for="last-name" class="block text-sm font-medium text-gray-700">Last name</label>
+							<input type="text" name="last-name" id="last-name" autocomplete="family-name"
+								   class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+						</div>
+
+						<div class="col-span-6 sm:col-span-4">
+							<label for="email-address" class="block text-sm font-medium text-gray-700">Email
+								address</label>
+							<input type="text" name="email-address" id="email-address" autocomplete="email"
+								   class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+						</div>
+
+						<div class="col-span-6 sm:col-span-3">
+							<label for="country" class="block text-sm font-medium text-gray-700">Country</label>
+							<select id="country" name="country" autocomplete="country-name"
+									class="mt-1 block w-full bg-white border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+								<option>United States</option>
+								<option>Canada</option>
+								<option>Mexico</option>
+							</select>
+						</div>
+
+						<div class="col-span-6">
+							<label for="street-address" class="block text-sm font-medium text-gray-700">Street
+								address</label>
+							<input type="text" name="street-address" id="street-address" autocomplete="street-address"
+								   class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+						</div>
+
+						<div class="col-span-6 sm:col-span-6 lg:col-span-2">
+							<label for="city" class="block text-sm font-medium text-gray-700">City</label>
+							<input type="text" name="city" id="city" autocomplete="address-level2"
+								   class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+						</div>
+
+						<div class="col-span-6 sm:col-span-3 lg:col-span-2">
+							<label for="region" class="block text-sm font-medium text-gray-700">State / Province</label>
+							<input type="text" name="region" id="region" autocomplete="address-level1"
+								   class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+						</div>
+
+						<div class="col-span-6 sm:col-span-3 lg:col-span-2">
+							<label for="postal-code" class="block text-sm font-medium text-gray-700">ZIP / Postal
+								code</label>
+							<input type="text" name="postal-code" id="postal-code" autocomplete="postal-code"
+								   class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+						</div>
+					</div>
+				</div>
+				<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
+					<button type="submit"
+							class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+						Save
+					</button>
+				</div>
+			</div>
+		</form>
+
+		<form action="#" method="POST">
+			<div class="shadow sm:rounded-md sm:overflow-hidden">
+				<div class="bg-white py-6 px-4 space-y-6 sm:p-6">
+					<div>
+						<h3 class="text-lg leading-6 font-medium text-gray-900">Notifications</h3>
+						<p class="mt-1 text-sm text-gray-500">Provide basic informtion about the job. Be specific with
+							the job title.</p>
+					</div>
+
+					<fieldset>
+						<legend class="text-base font-medium text-gray-900">By Email</legend>
+						<div class="mt-4 space-y-4">
+							<div class="flex items-start">
+								<div class="h-5 flex items-center">
+									<input id="comments" name="comments" type="checkbox"
+										   class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
+								</div>
+								<div class="ml-3 text-sm">
+									<label for="comments" class="font-medium text-gray-700">Comments</label>
+									<p class="text-gray-500">Get notified when someones posts a comment on a
+										posting.</p>
+								</div>
+							</div>
+							<div>
+								<div class="flex items-start">
+									<div class="h-5 flex items-center">
+										<input id="candidates" name="candidates" type="checkbox"
+											   class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
+									</div>
+									<div class="ml-3 text-sm">
+										<label for="candidates" class="font-medium text-gray-700">Candidates</label>
+										<p class="text-gray-500">Get notified when a candidate applies for a job.</p>
+									</div>
+								</div>
+							</div>
+							<div>
+								<div class="flex items-start">
+									<div class="h-5 flex items-center">
+										<input id="offers" name="offers" type="checkbox"
+											   class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
+									</div>
+									<div class="ml-3 text-sm">
+										<label for="offers" class="font-medium text-gray-700">Offers</label>
+										<p class="text-gray-500">Get notified when a candidate accepts or rejects an
+											offer.</p>
+									</div>
+								</div>
+							</div>
+						</div>
+					</fieldset>
+					<fieldset class="mt-6">
+						<legend class="text-base font-medium text-gray-900">Push Notifications</legend>
+						<p class="text-sm text-gray-500">These are delivered via SMS to your mobile phone.</p>
+						<div class="mt-4 space-y-4">
+							<div class="flex items-center">
+								<input id="push-everything" name="push-notifications" type="radio"
+									   class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
+								<label for="push-everything" class="ml-3">
+									<span class="block text-sm font-medium text-gray-700">Everything</span>
+								</label>
+							</div>
+							<div class="flex items-center">
+								<input id="push-email" name="push-notifications" type="radio"
+									   class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
+								<label for="push-email" class="ml-3">
+									<span class="block text-sm font-medium text-gray-700">Same as email</span>
+								</label>
+							</div>
+							<div class="flex items-center">
+								<input id="push-nothing" name="push-notifications" type="radio"
+									   class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
+								<label for="push-nothing" class="ml-3">
+									<span class="block text-sm font-medium text-gray-700">No push notifications</span>
+								</label>
+							</div>
+						</div>
+					</fieldset>
+				</div>
+				<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
+					<button type="submit"
+							class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600">
+						Save
+					</button>
+				</div>
+			</div>
+		</form>
+	</div>
+</div>
+</body>
+</html>
-- 
GitLab