Skip to content

Commit

Permalink
pass some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
NotAProton committed May 22, 2024
1 parent 0a86d20 commit 9dc36c8
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 11 deletions.
5 changes: 1 addition & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Run tests

on:
Expand Down Expand Up @@ -30,7 +27,7 @@ jobs:
with:
cache: 'npm'
- run: npm ci
- run: npm run format-check
- run: npm run check-format
test:
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"wrangler": "^3.0.0"
},
"dependencies": {
"@tsndr/cloudflare-worker-jwt": "^2.5.3",
"hono": "^4.3.9"
}
}
32 changes: 32 additions & 0 deletions src/handlers/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Context } from 'hono';
import { HTTPException } from 'hono/http-exception';
import jwt from '@tsndr/cloudflare-worker-jwt';

async function createJWT(email: string) {
const token = await jwt.sign({ email }, 'secret');
return token;
}

export default async function create(c: Context) {
let body;
let email: string | undefined, otp: string;
const emailPattern = /^[a-zA-Z]+\d{2}[a-zA-Z]{3}\d{1,3}@iiitkottayam\.ac\.in$/;
const otpPattern = /^\d{6}$/;
try {
body = await c.req.json();
email = body.email;
otp = body.otp;
} catch (e) {
throw new HTTPException(400);
}
if (!otp || !otpPattern.test(otp) || !email || !emailPattern.test(email)) {
throw new HTTPException(401);
}

return c.json({
status: 'success',
data: {
token: await createJWT(email),
},
});
}
5 changes: 4 additions & 1 deletion src/handlers/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ export default async function register(c: Context) {
const otp = Array.from({ length: 6 }, getRandomDigit).join('');
sendOTP(email, otp);

return c.status(200);
return c.json({
status: 'success',
data: null,
});
}
32 changes: 32 additions & 0 deletions src/handlers/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Context } from 'hono';
import { HTTPException } from 'hono/http-exception';
import jwt from '@tsndr/cloudflare-worker-jwt';

async function createJWT(email: string) {
const token = await jwt.sign({ email }, 'secret');
return token;
}

export default async function verify(c: Context) {
let body;
let email: string | undefined, otp: string;
const emailPattern = /^[a-zA-Z]+\d{2}[a-zA-Z]{3}\d{1,3}@iiitkottayam\.ac\.in$/;
const otpPattern = /^\d{6}$/;
try {
body = await c.req.json();
email = body.email;
otp = body.otp;
} catch (e) {
throw new HTTPException(400);
}
if (!otp || !otpPattern.test(otp) || !email || !emailPattern.test(email)) {
throw new HTTPException(401);
}

return c.json({
status: 'success',
data: {
token: await createJWT(email),
},
});
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Hono } from 'hono';
import register from './handlers/register';
import verify from './handlers/verify';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type Env = {
DATABASE_URL: string;
};

const app = new Hono<{ Bindings: Env }>();

app.post('/auth/register', register);
app.post('/auth/verify', verify);

export default app;
41 changes: 41 additions & 0 deletions src/lib/bcrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export async function hashPassword(password: string, providedSalt?: Uint8Array): Promise<string> {
const encoder = new TextEncoder();
// Use provided salt if available, otherwise generate a new one
const salt = providedSalt || crypto.getRandomValues(new Uint8Array(16));
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
'deriveBits',
'deriveKey',
]);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt'],
);
const exportedKey = (await crypto.subtle.exportKey('raw', key)) as ArrayBuffer;
const hashBuffer = new Uint8Array(exportedKey);
const hashArray = Array.from(hashBuffer);
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
const saltHex = Array.from(salt)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return `${saltHex}:${hashHex}`;
}

export async function verifyPassword(storedHash: string, passwordAttempt: string): Promise<boolean> {
const [saltHex, originalHash] = storedHash.split(':');
const matchResult = saltHex.match(/.{1,2}/g);
if (!matchResult) {
throw new Error('Invalid salt format');
}
const salt = new Uint8Array(matchResult.map((byte) => parseInt(byte, 16)));
const attemptHashWithSalt = await hashPassword(passwordAttempt, salt);
const [, attemptHash] = attemptHashWithSalt.split(':');
return attemptHash === originalHash;
}
20 changes: 14 additions & 6 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,27 @@ import worker from '../src/index';
// `Request` to pass to `worker.fetch()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;

describe('Hello World worker', () => {
it('responds with Hello World! (unit style)', async () => {
describe('worker', () => {
it('responds with 404 on index (unit style)', async () => {
const request = new IncomingRequest('http://example.com');
// Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
expect(response.status).toBe(404);
});

it('responds with Hello World! (integration style)', async () => {
const response = await SELF.fetch('https://example.com');
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
it('rejects incorrect accounts', async () => {
let response = await SELF.fetch('http://example.com/auth/register', {
method: 'POST',
body: JSON.stringify({ email: 'notanemail', password: 'password' }),
});
expect(response.status).toBe(400);
response = await SELF.fetch('http://example.com/auth/register', {
method: 'POST',
body: JSON.stringify({ email: '[email protected]', password: 'password' }),
});
expect(response.status).toBe(400);
});
});
11 changes: 11 additions & 0 deletions test/lib.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// test/index.spec.ts
import { describe, it, expect } from 'vitest';
import { hashPassword, verifyPassword } from '../src/lib/bcrypt';

describe('bcrypt', () => {
it('hashes and verifies passwords', async () => {
const password = 'password';
const hashed = await hashPassword(password);
expect(await verifyPassword(hashed, password)).toBe(true);
});
});

0 comments on commit 9dc36c8

Please sign in to comment.