Skip to content

Commit f6345bf

Browse files
author
Dylan Bulmer
authored
Add email sign-in support (#7)
* initialize core services * add instance details; add user model * update user model * init project config; add config tests; fix app tests * update examples, adjust config test * update project schema; prepare for json validation * update npm publish * slight tweaks * update deps; modify models * fix typos/task naming * start email auth * fix regex issues
1 parent 19308a5 commit f6345bf

35 files changed

+2571
-8
lines changed

.github/workflows/npm-publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
name: Node.js Package
55

66
on:
7+
workflow_dispatch:
78
release:
89
types: [created]
910

@@ -41,7 +42,6 @@ jobs:
4142
runs-on: ubuntu-latest
4243
steps:
4344
- uses: actions/checkout@v3
44-
# Setup .npmrc file to publish to GitHub Packages
4545
- uses: actions/setup-node@v3
4646
with:
4747
node-version: "16.x"
@@ -53,6 +53,6 @@ jobs:
5353
with:
5454
name: lib
5555
path: lib/
56-
- run: cd lib && yarn publish
56+
- run: cd lib && yarn publish --access public
5757
env:
5858
NODE_AUTH_TOKEN: ${{secrets.NPM_ACCESS_TOKEN}}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ web_modules/
7373
.yarn-integrity
7474

7575
# dotenv environment variable files
76-
# .env
76+
.env
7777
# .env.development.local
7878
# .env.test.local
7979
# .env.production.local

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
This is an open-sourced customizable annotation tool for researchers and industry.
66

7+
We are designing and developing Codr is a way that is exapandable and requires
8+
very little overhaul when an existing feature changes or a new feature is added.
9+
710
## TODO:
811

912
- [ ] Build the framework
@@ -12,4 +15,4 @@ This is an open-sourced customizable annotation tool for researchers and industr
1215

1316
## Getting started
1417

15-
This may get written soon.
18+
This may get written soon.

package.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,38 @@
1515
"lint": "eslint -c .eslintrc.json --ignore-path .eslintignore --ext .ts src",
1616
"preversion": "yarn lint",
1717
"version": "yarn format && git add -A src",
18-
"postversion": "git push && git push --tags"
18+
"postversion": "git push && git push --tags",
19+
"convert": "taplo get -f src/examples/project.toml -o json > src/examples/project.json"
1920
},
2021
"devDependencies": {
2122
"@swc/cli": "^0.1.57",
2223
"@swc/core": "^1.3.3",
2324
"@swc/jest": "^0.2.22",
25+
"@taplo/cli": "^0.5.2",
2426
"@types/jest": "^29.0.3",
27+
"@types/jsonwebtoken": "^8.5.9",
2528
"@types/node": "^18.7.21",
29+
"@types/nodemailer": "^6.4.6",
30+
"@types/uuid": "^9.0.0",
2631
"@typescript-eslint/eslint-plugin": "^5.38.0",
2732
"@typescript-eslint/parser": "^5.38.0",
33+
"dotenv": "^16.0.2",
2834
"eslint": "^8.24.0",
2935
"eslint-config-prettier": "^8.5.0",
3036
"eslint-plugin-jest": "^27.0.4",
3137
"jest": "^29.0.3",
3238
"prettier": "^2.7.1",
3339
"typescript": "^4.8.3"
40+
},
41+
"dependencies": {
42+
"@aws-sdk/client-ses": "^3.226.0",
43+
"@casl/ability": "^6.3.2",
44+
"@casl/mongoose": "^7.1.2",
45+
"ajv": "^8.11.0",
46+
"jsonschema": "^1.4.1",
47+
"jsonwebtoken": "^8.5.1",
48+
"mongoose": "^6.6.2",
49+
"nodemailer": "^6.8.0",
50+
"uuid": "^9.0.0"
3451
}
3552
}

src/__tests__/App.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import dotenv from "dotenv";
2+
import Codr, { App } from "../";
3+
4+
dotenv.config();
5+
6+
describe("App configuration", () => {
7+
const instance1 = {
8+
name: "My App Instance",
9+
contact: {
10+
name: "Dylan",
11+
email: "dylan@dylanbulmer.com",
12+
},
13+
};
14+
const instance2 = JSON.parse(JSON.stringify(instance1));
15+
delete instance2.contact;
16+
const databaseUri = "mongodb://someUser:abc123@server-a9.host.com:41653,server-a2.host.com/testdb-2?replicaSet=rs-some2&replicaSet2=rs-some.2";
17+
18+
// create app from Codr singleton
19+
const app1 = new Codr.App({ databaseUri: databaseUri, instance: instance1 });
20+
// create app from cherry picked import
21+
const app2 = new App({ databaseUri: databaseUri, instance: instance2 });
22+
23+
it("does not throw an error", () => {
24+
expect(app1.databaseUri).toBe(
25+
"mongodb://someUser:abc123@server-a9.host.com:41653,server-a2.host.com/testdb-2?replicaSet=rs-some2&replicaSet2=rs-some.2",
26+
);
27+
expect(app1.instance?.contact?.name).toEqual("Dylan");
28+
29+
expect(app2.databaseUri).toBe(
30+
"mongodb://someUser:abc123@server-a9.host.com:41653,server-a2.host.com/testdb-2?replicaSet=rs-some2&replicaSet2=rs-some.2",
31+
);
32+
expect(app2.instance?.name).toEqual("My App Instance");
33+
});
34+
35+
it("throws no mongodb url given", () => {
36+
const t = () => {
37+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
38+
// @ts-ignore
39+
return new App({});
40+
};
41+
expect(t).toThrow(Error);
42+
expect(t).toThrow("No Mongodb url was given.");
43+
});
44+
45+
it("throws mongodb url invalid", () => {
46+
const t = () => {
47+
return new App({
48+
databaseUri: "mongodb:/123.abc.com:27017/test",
49+
instance: instance1,
50+
});
51+
};
52+
expect(t).toThrow(Error);
53+
expect(t).toThrow("Malformatted Mongodb url.");
54+
});
55+
56+
it("throws instance data missing", () => {
57+
const t2 = () => {
58+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
59+
// @ts-ignore
60+
return new App({ databaseUri: databaseUri });
61+
};
62+
63+
expect(t2).toThrow(Error);
64+
expect(t2).toThrow("No instance data was given.");
65+
});
66+
});

src/__tests__/Config.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import ProjectConfiguration, { ConfigOptions } from "../models/Config";
2+
import options from "../examples/project.json";
3+
4+
describe("Project Configuration", () => {
5+
const options2: ConfigOptions = JSON.parse(JSON.stringify(options));
6+
delete options2.general.slug;
7+
delete options2.general.guidelines;
8+
delete options2.general.bgColorClass;
9+
10+
it("does not throw an error", () => {
11+
const config = ProjectConfiguration.from(
12+
options as unknown as ConfigOptions,
13+
);
14+
15+
expect(config.bgColorClass).toBe("bg-pink-600");
16+
expect(config.title).toBe("My Project");
17+
expect(config.display.inputs[1]).toEqual({
18+
type: "text",
19+
language: "Java",
20+
value: "model.data.methods.*.src_code",
21+
});
22+
});
23+
24+
it("generates missing options", () => {
25+
const config = ProjectConfiguration.from(options2);
26+
27+
expect(config.slug).toBe(
28+
`my-project-${new Date().toISOString().split("T")[0]}`,
29+
);
30+
expect(config.display.inputs[1]).toEqual({
31+
type: "text",
32+
language: "Java",
33+
value: "model.data.methods.*.src_code",
34+
});
35+
});
36+
});

src/classes/Email.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const EmailRegex =
2+
/^([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|"(\[\]!#-[^-~ \t]|(\\[\t -~]))+")@([0-9A-Za-z]([0-9A-Za-z-]{0,61}[0-9A-Za-z])?(\.[0-9A-Za-z]([0-9A-Za-z-]{0,61}[0-9A-Za-z])?)*|\[((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|IPv6:((((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){6}|::((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){5}|[0-9A-Fa-f]{0,4}::((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){4}|(((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):)?(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}))?::((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){3}|(((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){0,2}(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}))?::((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){2}|(((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){0,3}(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}))?::(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):|(((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){0,4}(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}))?::)((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3})|(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){0,5}(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}))?::(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3})|(((0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}):){0,6}(0|[1-9A-Fa-f][0-9A-Fa-f]{0,3}))?::)|(?!IPv6:)[0-9A-Za-z-]*[0-9A-Za-z]:[!-Z^-~]+)])$/g;
3+
4+
export default class Email {
5+
private $email: string;
6+
7+
constructor(email: string) {
8+
this.$email = email.toLowerCase();
9+
}
10+
11+
get email() {
12+
return this.$email;
13+
}
14+
15+
get isValid() {
16+
return this.validate();
17+
}
18+
19+
private validate() {
20+
// regex scraped from --> https://stackoverflow.com/a/201378/18004752
21+
return this.$email.search(EmailRegex) >= 0;
22+
}
23+
}

src/classes/Error.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Response, { IResponse } from "./Response";
2+
3+
interface IError {
4+
status: number;
5+
}
6+
7+
export default class Error<Details> extends Response<Details> {
8+
status: IError["status"];
9+
10+
constructor({ status, message }: IError & IResponse<Details>) {
11+
super({ message });
12+
this.status = status;
13+
}
14+
15+
toJSON() {
16+
return {
17+
status: this.status,
18+
...super.toJSON(),
19+
};
20+
}
21+
}

src/classes/JWT.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import jwt, { Algorithm } from "jsonwebtoken";
2+
import Error from "./Error";
3+
4+
export function verifyToken(token: string) {
5+
const bearerRegex = /^Bearer\s/;
6+
7+
if (token && bearerRegex.test(token)) {
8+
const newToken = token.replace(bearerRegex, "");
9+
jwt.verify(
10+
newToken,
11+
"secretKey",
12+
{
13+
issuer: process.env.JWT_ISSUER,
14+
},
15+
(error, decoded) => {
16+
if (error === null && decoded) {
17+
return true;
18+
}
19+
return false;
20+
},
21+
);
22+
} else {
23+
return false;
24+
}
25+
}
26+
27+
export function generateToken(payload: jwt.JwtPayload) {
28+
try {
29+
const signOpts: jwt.SignOptions = {
30+
issuer: process.env.JWT_ISSUER,
31+
algorithm: <Algorithm>process.env.JWT_ALGORITHM,
32+
};
33+
return jwt.sign(payload, <string>process.env.JWT_SECRET, signOpts);
34+
} catch (err) {
35+
throw new Error({ status: 500, message: <string>err });
36+
}
37+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
export interface RecipientEmailOptions {
2+
to: string;
3+
cc?: string[] | undefined;
4+
bcc?: string[] | undefined;
5+
}
6+
7+
export interface IGenericTemplate<T> {
8+
template: string;
9+
subject: string;
10+
requiredParams: T[];
11+
bcc?: string[];
12+
}
13+
14+
export default class GenericTemplate<T extends string>
15+
implements IGenericTemplate<T>
16+
{
17+
readonly template: string;
18+
readonly subject: string;
19+
readonly requiredParams: T[];
20+
bcc?: string[];
21+
22+
/**
23+
*
24+
* @param {string} template Email Template
25+
* @param {string} subject Email Subject
26+
* @param {T[]} requiredParams Required parameters for template
27+
*/
28+
constructor(template: string, subject: string, requiredParams: T[]) {
29+
this.template = template;
30+
this.subject = subject;
31+
this.requiredParams = requiredParams;
32+
}
33+
34+
get config() {
35+
return {
36+
subject: this.subject,
37+
from: process.env["EMAIL_FROM"] as string,
38+
bcc: this.bcc,
39+
};
40+
}
41+
42+
/**
43+
* @param {{[x: string]: string}} params Values to replace placeholders in template text.
44+
*/
45+
validate(params: Record<T, string>) {
46+
const missing = [];
47+
for (const opt of this.requiredParams) {
48+
if (Object.prototype.hasOwnProperty.call(params, opt)) {
49+
// check to makesure the option is not empty
50+
if (params[opt] === "") {
51+
missing.push(opt);
52+
}
53+
} else {
54+
// if the option is complete missing, add it to the list
55+
missing.push(opt);
56+
}
57+
}
58+
59+
if (missing.length) {
60+
throw new Error(`Missing template parameters: ${missing.join(", ")}`);
61+
} else {
62+
return true;
63+
}
64+
}
65+
66+
html(options: Record<T, string>) {
67+
return new Promise<string>((resolve, reject) => {
68+
try {
69+
this.validate(options);
70+
const content = this.template.replace(
71+
/{(\w*)}/g,
72+
function replacer(m, key: T) {
73+
if (Object.prototype.hasOwnProperty.call(options, key)) {
74+
return options[key];
75+
} else {
76+
throw new Error(`Missing parameter: ${key}`);
77+
}
78+
},
79+
);
80+
resolve(this.wrapperHTML({ content }));
81+
} catch (e) {
82+
reject(e);
83+
}
84+
});
85+
}
86+
87+
/**
88+
* Wrapper HTML
89+
* @param {Object} Options
90+
* @param {string} Options.content
91+
* @returns
92+
*/
93+
private wrapperHTML({ content }: { content: string }) {
94+
return `<body
95+
style="background: #f3f4f6; padding: 2em; font-size:16px; font-family:source-sans-pro, Roboto, sans-serif;"
96+
>
97+
<div style=" text-align: center;">
98+
<div
99+
style="background: white; padding: 2em; border-radius: 0.5em; max-width: 500px; text-align: left; margin: auto;"
100+
>
101+
${content}
102+
103+
Best,
104+
Your Codr Team
105+
support@codrjs.com
106+
</div>
107+
</div>
108+
</body>`.replace(/[\n]*/g, "");
109+
}
110+
}
111+
112+
/**
113+
* Email header image code
114+
*
115+
* <img
116+
* src="cid:logo"
117+
* alt="TrustedRentr Logo"
118+
* style="display: block; width: 100%; height: 48px; margin-bottom: 2em; object-fit: contain;"
119+
* />
120+
*/

0 commit comments

Comments
 (0)