diff --git a/.github/workflows/deploy-functions.yml b/.github/workflows/deploy-functions.yml index fb87923..fadea6d 100644 --- a/.github/workflows/deploy-functions.yml +++ b/.github/workflows/deploy-functions.yml @@ -29,5 +29,5 @@ jobs: cd .. cd functions npm install - - name: deploy functions + - name: deploy functions and hosting routes run: firebase deploy -f --token ${FIREBASE_TOKEN} \ No newline at end of file diff --git a/README.md b/README.md index 952b62f..dc0d753 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,66 @@ pm2 start ecosystem.config.js pm2 save ``` +Put behind a NGINX reverse proxy with this config (encryption by LetsEncrypt) + +``` +server { + server_name account.lingdocs.com; + + location / { + proxy_pass http://localhost:4000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Access-Control-Allow-Origin *; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + error_page 500 /500.json; + location /500.json { + return 500 '{"ok":false,"error":"500 Internal Server Error"}'; + } + + error_page 502 /502.json; + location /502.json { + return 502 '{"ok":false,"error":"502 Bad Gateway"}'; + } + + error_page 503 /503.json; + location /503.json { + return 503 '{"ok":false,"error":"503 Service Temporarily Unavailable"}'; + } + + error_page 504 /504.json; + location /504.json { + return 504 '{"ok":false,"error":"504 Gateway Timeout"}'; + } + + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/account.lingdocs.com/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/account.lingdocs.com/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = account.lingdocs.com) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + server_name account.lingdocs.com; + + listen 80; + listen [::]:80; + return 404; # managed by Certbot + +} +``` + #### CouchDB When a user upgrades their account level to `student` or `editor`: diff --git a/account/README.md b/account/README.md deleted file mode 100644 index 5c067da..0000000 --- a/account/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# auth.lingdocs.com - -Auth service for LingDocs (in progress, not usable yet) - diff --git a/firebase.json b/firebase.json index 8c8dcbe..6a4a9a6 100644 --- a/firebase.json +++ b/firebase.json @@ -7,8 +7,8 @@ "public": "public", "rewrites": [ { - "source": "/authory", - "function": "authory" + "source": "/testme", + "function": "testme" } ] } diff --git a/functions/package-lock.json b/functions/package-lock.json index 9b7b716..3a53205 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -410,6 +410,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.55.tgz", "integrity": "sha512-koZJ89uLZufDvToeWO5BrC4CR4OUfHnUz2qoPs/daQH6qq3IN62QFxCTZ+bKaCE0xaoCAJYE4AXre8AbghCrhg==" }, + "@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", @@ -540,6 +550,12 @@ "retry": "0.12.0" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, "axios": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", @@ -648,6 +664,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -731,6 +756,12 @@ "ms": "2.1.2" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -980,6 +1011,17 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", diff --git a/functions/package.json b/functions/package.json index cb23eb9..443b24d 100644 --- a/functions/package.json +++ b/functions/package.json @@ -22,12 +22,14 @@ "firebase-functions": "^3.11.0", "google-spreadsheet": "^3.1.15", "nano": "^9.0.3", + "node-fetch": "^2.6.1", "react": "^17.0.1", "react-bootstrap": "^1.5.1", "react-dom": "^17.0.1" }, "devDependencies": { "@types/jest": "^26.0.20", + "@types/node-fetch": "^2.5.12", "firebase-functions-test": "^0.2.0", "typescript": "^3.8.0" }, diff --git a/functions/src/index.ts b/functions/src/index.ts index ae63665..d4f2f31 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,75 +1,26 @@ import * as functions from "firebase-functions"; -import publish from "./publish"; -import { - receiveSubmissions, -} from "./submissions"; -import generatePassword from "./generate-password"; -import * as BT from "../../website/src/lib/backend-types" +import fetch from "node-fetch"; import cors from "cors"; -import * as admin from "firebase-admin"; -import { getUserDbName } from "./lib/userDbName"; +// import publish from "./publish"; +// import * as BT from "../../website/src/lib/backend-types" -const nano = require("nano")(functions.config().couchdb.couchdb_url); -const usersDb = nano.db.use("_users"); - -admin.initializeApp(); - -const validateFirebaseIdToken = async (req: any, res: any, next: any) => { - if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) && - !(req.cookies && req.cookies.__session)) { - res.status(403).send({ message: "Unauthorized" }); - return; - } - - let idToken; - if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { - // Read the ID Token from the Authorization header. - idToken = req.headers.authorization.split('Bearer ')[1]; - } else if(req.cookies) { - // Read the ID Token from cookie. - idToken = req.cookies.__session; - } else { - // No cookie - res.status(403).send({ message: "Unauthorized" }); - return; - } - - try { - const decodedIdToken = await admin.auth().verifyIdToken(idToken); - req.user = decodedIdToken; - next(); - return; - } catch (error) { - console.error('Error while verifying Firebase ID token:', error); - res.status(403).send({ message: "Unauthorized" }); - return; - } -}; - -const isEditor = async (req: any) => { - const uid = req.user.uid as string; - const couchDbUser = await getCouchDbUser(uid); - return !!couchDbUser && couchDbUser.level === "editor"; -} - -export const publishDictionary = functions - .region("europe-west1") - .runWith({ - timeoutSeconds: 200, - memory: "2GB" - }) +export const testme = functions + // .runWith({ + // timeoutSeconds: 200, + // memory: "2GB" + // }) .https.onRequest((req, res) => { - return cors({ origin: true })(req, res, () => { - validateFirebaseIdToken(req, res, async () => { - try { - const response = await publish(); - return res.send(response); - } catch (error) { - return res.status(500).send({ - error: error.toString(), - }); - } - }); + return cors({ credentials: true, origin: /\.lingdocs\.com$/ })(req, res, () => { + const { headers: { cookie }} = req; + if (!cookie) { + return res.status(401).send({ ok: false, error: "unauthorized" }); + } + fetch("https://account.lingdocs.com/api/user", { + headers: { cookie }, + }).then(r => r.json()).then(r => { + res.send({ ok: true, r }); + }).catch((error) => res.send({ ok: false, error })); + return; }); }); @@ -82,151 +33,30 @@ export const submissions = functions memory: "1GB" }) .https.onRequest((req, res) => { - return cors({ origin: true })(req, res, () => { - validateFirebaseIdToken(req, res, async () => { - if (!Array.isArray(req.body)) { - res.status(400).send({ - ok: false, - error: "invalid submission", - }); - return; - } - const suggestions = req.body as BT.SubmissionsRequest; - // @ts-ignore - const uid = req.user.uid as string; - const editor = await isEditor(req); - try { - const response = await receiveSubmissions(suggestions, editor); - // TODO: WARN IF ANY OF THE EDITS DIDN'T HAPPEN - res.send(response); - return; - } catch (error) { - console.error(error); - return res.status(500).send({ - error: error.toString(), - }); - }; - }).catch(console.error); + res.send({ ok: false, error: "function under maintenance" }); + // return cors({ origin: true })(req, res, () => { + // validateFirebaseIdToken(req, res, async () => { + // if (!Array.isArray(req.body)) { + // res.status(400).send({ + // ok: false, + // error: "invalid submission", + // }); + // return; + // } + // const suggestions = req.body as BT.SubmissionsRequest; + // // @ts-ignore + // const uid = req.user.uid as string; + // const editor = await isEditor(req); + // try { + // const response = await receiveSubmissions(suggestions, editor); + // // TODO: WARN IF ANY OF THE EDITS DIDN'T HAPPEN + // res.send(response); + // return; + // } catch (error) { + // console.error(error); + // return res.status(500).send({ + // error: error.toString(), + // }); + // }; + // }).catch(console.error); }); -}); - -export const getUserInfo = functions.region("europe-west1").https.onRequest((req, res) => { - return cors({ origin: true })(req, res, () => { - validateFirebaseIdToken(req, res, async () => { - try { - // @ts-ignore - const uid = req.user.uid as string; - const user = await getCouchDbUser(uid); - if (!user) { - const noneFound: BT.GetUserInfoResponse = { - ok: true, - message: "no couchdb user found", - }; - res.send(noneFound); - return; - } - const userFound: BT.GetUserInfoResponse = { ok: true, user }; - res.send(userFound); - return; - } catch(error) { - console.error(error); - res.status(500).send({ - ok: false, - error: error.message, - }); - } - }).catch(console.error); - }); -}); - -// export const cleanUpUser = functions -// .region("europe-west1") -// .auth.user().onDelete(async (user) => { -// const couchDbUser = await getCouchDbUser(user.uid); -// if (!couchDbUser) return; -// await usersDb.destroy( -// `org.couchdb.user:${user.uid}`, -// couchDbUser._rev, -// ); -// try { -// await nano.db.destroy(getUserDbName(user.uid)); -// } catch (e) { -// console.log("errored destroying", e); -// }; -// }); - -export const upgradeUser = functions.region("europe-west1").https.onRequest((req, res) => { - return cors({ origin: true })(req, res, () => { - validateFirebaseIdToken(req, res, async () => { - const password = (req.body.password || "") as string; - const studentPassword = functions.config().upgrades.student_password as string; - if (password.toLowerCase() !== studentPassword.toLowerCase()) { - const wrongPass: BT.UpgradeUserResponse = { - ok: false, - error: "incorrect password", - }; - res.send(wrongPass); - return; - } - // @ts-ignore - const uid = req.user.uid; - const couchDbUser = await getCouchDbUser(uid); - if (couchDbUser) { - const alreadyUpgraded: BT.UpgradeUserResponse = { - ok: true, - message: "user already upgraded", - }; - res.send(alreadyUpgraded); - return; - } - const user = await admin.auth().getUser(uid); - const userdbPassword = generatePassword(); - const newCouchDbUser: BT.CouchDbUser = { - _id: `org.couchdb.user:${user.uid}`, - type: "user", - name: user.uid, - email: user.email || "", - providerData: user.providerData, - displayName: user.displayName || "", - roles: [], - password: userdbPassword, - level: "student", - userdbPassword, - }; - await usersDb.insert(newCouchDbUser); - // create wordlist database for user - const userDbName = getUserDbName(user.uid); - await nano.db.create(userDbName); - const securityInfo = { - admins: { - names: [user.uid], - roles: ["_admin"] - }, - members: { - names: [user.uid], - roles: ["_admin"], - }, - }; - const userDb = nano.db.use(userDbName); - await userDb.insert(securityInfo, "_security"); - // TODO: SET THE USERDBPASSWORD TO BE userdbPassword; - const upgraded: BT.UpgradeUserResponse = { - ok: true, - message: "user upgraded to student", - }; - res.send(upgraded); - }).catch(console.error); - }); -}); - -async function getCouchDbUser(uid: string): Promise { - const user = await usersDb.find({ - selector: { - name: uid, - } - }); - if (!user.docs.length) { - return undefined; - } - return user.docs[0] as BT.CouchDbUser; -} \ No newline at end of file diff --git a/functions/src/publish.ts b/functions/src/publish.ts index a6e4c4a..e7d6278 100644 --- a/functions/src/publish.ts +++ b/functions/src/publish.ts @@ -16,7 +16,7 @@ import { // } from "./word-list-maker"; import { PublishDictionaryResponse, -} from "../../website/src/lib/backend-types"; +} from "../../website/src/lib/functions-types"; import { Storage } from "@google-cloud/storage"; const storage = new Storage({ projectId: "lingdocs", diff --git a/functions/src/submissions.ts b/functions/src/submissions.ts index 73e56f6..b36a57c 100644 --- a/functions/src/submissions.ts +++ b/functions/src/submissions.ts @@ -4,7 +4,7 @@ import { dictionaryEntryBooleanFields, dictionaryEntryNumberFields, } from "@lingdocs/pashto-inflector"; -import * as BT from "../../website/src/lib/backend-types"; +import * as FT from "../../website/src/lib/functions-types"; import * as functions from "firebase-functions"; const fieldsForEdit = [ @@ -17,7 +17,7 @@ const fieldsForEdit = [ const nano = require("nano")(functions.config().couchdb.couchdb_url); const reviewTasksDb = nano.db.use("review-tasks"); -export async function receiveSubmissions(e: BT.SubmissionsRequest, editor: boolean): Promise { +export async function receiveSubmissions(e: FT.SubmissionsRequest, editor: boolean): Promise { const { edits, reviewTasks } = sortSubmissions(e); // TODO: BETTER PROMISE MULTI-TASKING @@ -111,11 +111,11 @@ export async function receiveSubmissions(e: BT.SubmissionsRequest, editor: boole } type SortedSubmissions = { - edits: BT.Edit[], - reviewTasks: BT.ReviewTask[], + edits: FT.Edit[], + reviewTasks: FT.ReviewTask[], }; -export function sortSubmissions(submissions: BT.Submission[]): SortedSubmissions { +export function sortSubmissions(submissions: FT.Submission[]): SortedSubmissions { const base: SortedSubmissions = { edits: [], reviewTasks: [], @@ -131,12 +131,12 @@ export function sortSubmissions(submissions: BT.Submission[]): SortedSubmissions } type SortedEdits = { - entryEdits: BT.EntryEdit[], - newEntries: BT.NewEntry[], - entryDeletions: BT.EntryDeletion[], + entryEdits: FT.EntryEdit[], + newEntries: FT.NewEntry[], + entryDeletions: FT.EntryDeletion[], } -export function sortEdits(edits: BT.Edit[]): SortedEdits { +export function sortEdits(edits: FT.Edit[]): SortedEdits { const base: SortedEdits = { entryEdits: [], newEntries: [], diff --git a/website/src/screens/Account.tsx b/website/src/screens/Account.tsx index f1c37e7..f53080b 100644 --- a/website/src/screens/Account.tsx +++ b/website/src/screens/Account.tsx @@ -31,6 +31,11 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs setUpgradeError(""); setWaiting(false); window.addEventListener("message", handleIncomingMessage); + console.log("send test func"); + fetch("https://functions.lingdocs.com/testme", { credentials: "include" }).then((res) => res.text()).then((res) => { + console.log("test func here"); + console.log(res); + }); return () => { window.removeEventListener("message", handleIncomingMessage); };