test backend lingdocs auth
This commit is contained in:
parent
1dcf1bf266
commit
41f401aa45
|
@ -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}
|
60
README.md
60
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`:
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# auth.lingdocs.com
|
||||
|
||||
Auth service for LingDocs (in progress, not usable yet)
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
"public": "public",
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/authory",
|
||||
"function": "authory"
|
||||
"source": "/testme",
|
||||
"function": "testme"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
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);
|
||||
});
|
||||
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;
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue