Skip to content

Commit

Permalink
feat: use mongo in lieu of couch to power data. Add auth middleware. …
Browse files Browse the repository at this point in the history
…Bump deps #2 #8
  • Loading branch information
TillaTheHun0 committed Aug 24, 2023
1 parent 8c1f768 commit 77684c0
Show file tree
Hide file tree
Showing 10 changed files with 1,978 additions and 181 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This recipe uses Render's [Infra as Code](https://render.com/docs/infrastructure
Using the `render.yaml` file, 6 `docker` services are provisioned:

- [x] The hyper Service, using the RESTful api, so that you may consume your services over Http.
- [x] CouchDB to power hyper data
- [x] MongoDB to power hyper data
- [x] Redis to power hyper cache
- [ ] Elasticsearch to power hyper search
- [ ] Minio to power hyper storage
Expand Down
28 changes: 0 additions & 28 deletions app/auth.ts

This file was deleted.

25 changes: 14 additions & 11 deletions app/deps.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
export { z } from 'https://deno.land/x/[email protected]/mod.ts';
export type {
NextFunction,
Opine,
OpineRequest,
OpineResponse,
} from 'https://deno.land/x/[email protected]/mod.ts';
// See https://deno.land/manual@v1.31.1/advanced/typescript/types#providing-types-when-importing
// @deno-types="npm:@types/express@^4.17"
export { default as express } from 'npm:[email protected]';

export { default as hyper } from 'https://x.nest.land/[email protected]/mod.js';
export { default as app } from 'https://x.nest.land/[email protected]/mod.js';
export * as jwt from 'https://deno.land/x/[email protected]/mod.ts';

export { default as couchdb } from 'https://x.nest.land/[email protected]/mod.js';
export { default as redis } from 'https://x.nest.land/[email protected]/mod.js';
export { default as elasticsearch } from 'https://x.nest.land/[email protected]/mod.js';
// hyper core
export { default as hyper } from 'https://raw.githubusercontent.com/hyper63/hyper/hyper%40v4.2.0/packages/core/mod.ts';
// hyper driving adapter
export { default as app } from 'https://raw.githubusercontent.com/hyper63/hyper/hyper-app-express%40v1.2.0/packages/app-express/mod.ts';

// hyper driven adapters
export { default as mongodb } from 'https://raw.githubusercontent.com/hyper63/hyper-adapter-mongodb/v3.1.3/mod.ts';
export { default as redis } from 'https://raw.githubusercontent.com/hyper63/hyper-adapter-redis/v3.0.0/mod.js';
export { default as hooks } from 'https://raw.githubusercontent.com/hyper63/hyper-adapter-hooks/v1.0.6/mod.js';
export { default as elasticsearch } from 'https://raw.githubusercontent.com/hyper63/hyper-adapter-elasticsearch/v2.0.2/mod.js';
75 changes: 50 additions & 25 deletions app/mod.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,59 @@
import { authMiddleware } from './auth.ts';
import { app, couchdb, elasticsearch, hyper, redis, z } from './deps.ts';
import { app, elasticsearch, type express, hyper, mongodb, redis } from './deps.ts';
import { env, verifyAuthorizationHeader } from './utils.ts';

function env(key: string): string {
const res = z.string().min(1).safeParse(Deno.env.get(key));
if (!res.success) {
console.error(`Error with ENV VAR: ${key}`);
throw res.error;
}
return res.data;
}
/**
* Given a sub and secret, return a hyper middleware that will
* check that all incoming requests have a properly signed jwt token
* in the authorization header
*/
const authMiddleware =
({ sub, secret }: { sub: string; secret: string }) => (app: express.Express) => {
const verify = verifyAuthorizationHeader({ sub, secret });
app.use(async (req, _res, next) => {
await verify(req.get('authorization') || 'Bearer notoken')
.then(() => next())
// pass error to next, triggering the next error middleware to take over
.catch(next);
});

const COUCH = `http://${env('COUCHDB_USER')}:${env('COUCHDB_PASSWORD')}@${
env(
'COUCHDB_HOST',
)
}:5984`;
app.use(
(
// deno-lint-ignore no-explicit-any
err: any,
_req: express.Request,
res: express.Response,
next: express.NextFunction,
): unknown => {
if (err && err.name === 'UnauthorizedError') {
return res.status(401).send({ ok: false, msg: 'not authorized' });
}
// Trigger the next error handler
next(err);
},
);

const REDIS = {
hostname: env('REDIS_HOST'),
port: env('REDIS_PORT'),
};

const ELASTICSEARCH = `http://${env('ELASTICSEARCH_HOST')}`;
return app;
};

export default hyper({
app,
adapters: [
{ port: 'data', plugins: [couchdb({ url: COUCH })] },
{ port: 'cache', plugins: [redis(REDIS)] },
{ port: 'search', plugins: [elasticsearch({ url: ELASTICSEARCH })] },
{
port: 'data',
plugins: [
mongodb({
url: `mongodb://${env('MONGO_USERNAME')}:${env('MONGO_PASSWORD')}@${env('MONGO_HOST')}`,
}),
],
},
{
port: 'cache',
plugins: [
// @ts-ignore incorrect types in the adapter, so safe to ignore for now
redis({ hostname: env('REDIS_HOST'), port: env('REDIS_PORT') }),
],
},
{ port: 'search', plugins: [elasticsearch({ url: `http://${env('ELASTICSEARCH_HOST')}` })] },
],
middleware: [authMiddleware(env('SECRET'))],
middleware: [authMiddleware({ sub: env('SUB'), secret: env('SECRET') })],
});
75 changes: 75 additions & 0 deletions app/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { assert, assertEquals, assertThrows } from 'https://deno.land/[email protected]/assert/mod.ts';
import * as jwt from 'https://deno.land/x/[email protected]/mod.ts';

import { env, verifyAuthorizationHeader } from './utils.ts';

Deno.test('utils', async (t) => {
await t.step('env', async (t) => {
await t.step('it should return the environment variable value', () => {
Deno.env.set('TEST', 'foobar');
assertEquals(env('TEST'), 'foobar');
});

await t.step(
'it should throw if the environment variable is not defined',
() => {
Deno.env.delete('TEST');
assertThrows(() => env('TEST'));
},
);
});

await t.step('verifyAuthorizationHeader', async (t) => {
const verify = verifyAuthorizationHeader({ sub: 'foo', secret: 'bar' });

// deno-lint-ignore no-explicit-any
const createToken = ({ sub, secret }: any, headers: any) =>
jwt.create(headers, { sub }, secret);

await t.step('it should verify the header value', async () => {
const token = await createToken(
{ sub: 'foo', secret: 'bar' },
{ alg: 'HS256', type: 'JWT' },
);
await verify(`Bearer ${token}`)
.then(() => assert(true))
.catch((err) => assert(false, err));
});

await t.step(
'it should throw an UnauthorizedError if token signing verification fails',
async () => {
const token = await createToken(
{ sub: 'foo', secret: 'NOT_RIGHT' },
{
alg: 'HS256',
type: 'JWT',
},
);
await verify(`Bearer ${token}`)
.then(() => assert(false, 'should have thrown'))
.catch((err) => {
assertEquals(err.name, 'UnauthorizedError');
});
},
);

await t.step(
'it should throw an UnauthorizedError if the sub in the payload is incorrect',
async () => {
const token = await createToken(
{ sub: 'NOT_RIGHT', secret: 'bar' },
{
alg: 'HS256',
type: 'JWT',
},
);
await verify(`Bearer ${token}`)
.then(() => assert(false, 'should have thrown'))
.catch((err) => {
assertEquals(err.name, 'UnauthorizedError');
});
},
);
});
});
23 changes: 23 additions & 0 deletions app/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { jwt, z } from './deps.ts';

export function env(key: string): string {
const res = z.string().min(1).safeParse(Deno.env.get(key));
if (!res.success) {
console.error(`Error with ENV VAR: ${key}`);
throw res.error;
}
return res.data;
}

export const verifyAuthorizationHeader =
({ sub, secret }: { sub: string; secret: string }) => async (header: string) => {
const payload = await jwt
.verify(header.split(' ').pop() as string, secret, 'HS256')
.catch(() => {
throw { name: 'UnauthorizedError' };
});
/**
* Confirm sub matches
*/
if (payload.sub !== sub) throw { name: 'UnauthorizedError' };
};
7 changes: 1 addition & 6 deletions data/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
# The official CouchDB Docker image
FROM couchdb:3

# Setup CouchDB to run as a single node
RUN echo '[couchdb]' > /opt/couchdb/etc/local.d/10-single-node.ini
RUN echo 'single_node=true' >> /opt/couchdb/etc/local.d/10-single-node.ini
FROM mongo:7.0.0
14 changes: 7 additions & 7 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
"$schema": "https://deno.land/x/deno/cli/schemas/config-file.v1.json",
"description": "Recipe for deploying the hyper Service Framework to Render using their Infra as Code feature",
"tasks": {
"cache": "deno cache --lock=deno.lock --lock-write ./app/deps.ts",
"prepare": "deno run -A npm:husky@^8 install",
"staged": "deno run -A npm:lint-staged@^13"
"staged": "deno run -A npm:lint-staged@^13",
"test": "deno lint && deno test -A"
},
"lint": {
"rules": {
"tags": ["recommended"]
}
},
"fmt": {
"options": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": true
}
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": true
},
"lock": false
}
Loading

0 comments on commit 77684c0

Please sign in to comment.