test backend lingdocs auth

This commit is contained in:
lingdocs 2021-08-24 14:24:53 +04:00
parent 1dcf1bf266
commit 41f401aa45
10 changed files with 167 additions and 232 deletions

View File

@ -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}

View File

@ -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`:

View File

@ -1,4 +0,0 @@
# auth.lingdocs.com
Auth service for LingDocs (in progress, not usable yet)

View File

@ -7,8 +7,8 @@
"public": "public",
"rewrites": [
{
"source": "/authory",
"function": "authory"
"source": "/testme",
"function": "testme"
}
]
}

View File

@ -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",

View File

@ -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"
},

View File

@ -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<undefined | BT.CouchDbUser> {
const user = await usersDb.find({
selector: {
name: uid,
}
});
if (!user.docs.length) {
return undefined;
}
return user.docs[0] as BT.CouchDbUser;
}

View File

@ -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",

View File

@ -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<BT.SubmissionsResponse> {
export async function receiveSubmissions(e: FT.SubmissionsRequest, editor: boolean): Promise<FT.SubmissionsResponse> {
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: [],

View File

@ -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);
};