diff --git a/apidoc.json b/apidoc.json index 2ed2ebc..c64410a 100644 --- a/apidoc.json +++ b/apidoc.json @@ -1,6 +1,6 @@ { - "name": "openauth", - "description": "Open Auth REST API", - "title": "Open Auth REST", - "url": "/api" -} \ No newline at end of file + "name": "openauth", + "description": "Open Auth REST API", + "title": "Open Auth REST", + "url": "/api" +} diff --git a/locales/de.json b/locales/de.json index c44a514..19ab808 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,42 +1,42 @@ { - "User not found": "Benutzer nicht gefunden", - "Password or username wrong": "Passwort oder Benutzername falsch", - "Authorize %s": "Authorize %s", - "Login": "Einloggen", - "You are not logged in or your login is expired": "Du bist nicht länger angemeldet oder deine Anmeldung ist abgelaufen.", - "Username or Email": "Benutzername oder Email", - "Password": "Passwort", - "Next": "Weiter", - "Register": "Registrieren", - "Mail": "Mail", - "Repeat Password": "Passwort wiederholen", - "Username": "Benutzername", - "Name": "Name", - "Registration code": "Registrierungs Schlüssel", - "You need to select one of the options": "Du musst eine der Optionen auswälen", - "Male": "Mann", - "Female": "Frau", - "Other": "Anderes", - "Registration code required": "Registrierungs Schlüssel benötigt", - "Username required": "Benutzername benötigt", - "Name required": "Name benötigt", - "Mail required": "Mail benötigt", - "The passwords do not match": "Die Passwörter stimmen nicht überein", - "Password is required": "Password benötigt", - "Invalid registration code": "Ungültiger Registrierungs Schlüssel", - "Username taken": "Benutzername nicht verfügbar", - "Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden", - "Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet", - "Administration": "Administration", - "Field {{field}} is not defined": "Feld {{field}} fehlt", - "Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Typ. Es sollte vom Typ {{type}} sein", - "Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen", - "Invalid token": "Ungültiger Token", - "By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen.", - "User": "User", - "No special token": "No special token", - "Login token invalid": "Login token invalid", - "No login token": "No login token", - "You are not logged in or your login is expired (Login token invalid)": "You are not logged in or your login is expired (Login token invalid)", - "You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)" -} \ No newline at end of file + "User not found": "Benutzer nicht gefunden", + "Password or username wrong": "Passwort oder Benutzername falsch", + "Authorize %s": "Authorize %s", + "Login": "Einloggen", + "You are not logged in or your login is expired": "Du bist nicht länger angemeldet oder deine Anmeldung ist abgelaufen.", + "Username or Email": "Benutzername oder Email", + "Password": "Passwort", + "Next": "Weiter", + "Register": "Registrieren", + "Mail": "Mail", + "Repeat Password": "Passwort wiederholen", + "Username": "Benutzername", + "Name": "Name", + "Registration code": "Registrierungs Schlüssel", + "You need to select one of the options": "Du musst eine der Optionen auswälen", + "Male": "Mann", + "Female": "Frau", + "Other": "Anderes", + "Registration code required": "Registrierungs Schlüssel benötigt", + "Username required": "Benutzername benötigt", + "Name required": "Name benötigt", + "Mail required": "Mail benötigt", + "The passwords do not match": "Die Passwörter stimmen nicht überein", + "Password is required": "Password benötigt", + "Invalid registration code": "Ungültiger Registrierungs Schlüssel", + "Username taken": "Benutzername nicht verfügbar", + "Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden", + "Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet", + "Administration": "Administration", + "Field {{field}} is not defined": "Feld {{field}} fehlt", + "Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Typ. Es sollte vom Typ {{type}} sein", + "Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen", + "Invalid token": "Ungültiger Token", + "By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen.", + "User": "User", + "No special token": "No special token", + "Login token invalid": "Login token invalid", + "No login token": "No login token", + "You are not logged in or your login is expired (Login token invalid)": "You are not logged in or your login is expired (Login token invalid)", + "You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)" +} diff --git a/locales/en.json b/locales/en.json index 71c3f86..eff50de 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,17 +1,17 @@ { - "Login": "Login", - "Username or Email": "Username or Email", - "Password": "Password", - "Next": "Next", - "Invalid code": "Invalid code", - "You are not logged in or your login is expired": "You are not logged in or your login is expired", - "User not found": "User not found", - "No special token": "No special token", - "You are not logged in or your login is expired(No special token)": "You are not logged in or your login is expired(No special token)", - "Special token invalid": "Special token invalid", - "You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)", - "No login token": "No login token", - "Login token invalid": "Login token invalid", - "Authorize %s": "Authorize %s", - "By clicking on ALLOW, you allow this app to access the requested recources.": "By clicking on ALLOW, you allow this app to access the requested recources." -} \ No newline at end of file + "Login": "Login", + "Username or Email": "Username or Email", + "Password": "Password", + "Next": "Next", + "Invalid code": "Invalid code", + "You are not logged in or your login is expired": "You are not logged in or your login is expired", + "User not found": "User not found", + "No special token": "No special token", + "You are not logged in or your login is expired(No special token)": "You are not logged in or your login is expired(No special token)", + "Special token invalid": "Special token invalid", + "You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)", + "No login token": "No login token", + "Login token invalid": "Login token invalid", + "Authorize %s": "Authorize %s", + "By clicking on ALLOW, you allow this app to access the requested recources.": "By clicking on ALLOW, you allow this app to access the requested recources." +} diff --git a/package-lock.json b/package-lock.json index 90b2929..b7f9ac4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2361,6 +2361,12 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "prettier": { + "version": "2.0.5", + "resolved": "https://npm.hibas123.de/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index deaa6c9..f17a292 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "watch-views": "cd views && npm run watch", "install-views_repo": "git submodule init && git submodule update && cd views_repo && npm install ", "build-views_repo": "cd views_repo && npm run build", - "watch-views_repo": "cd views_repo && npm run dev" + "watch-views_repo": "cd views_repo && npm run dev", + "format": "prettier --write ." }, "pipelines": { "install": [ @@ -47,6 +48,7 @@ "apidoc": "^0.20.0", "concurrently": "^5.1.0", "nodemon": "^2.0.2", + "prettier": "^2.0.5", "typescript": "^3.8.3" }, "dependencies": { diff --git a/src/api/admin/client.ts b/src/api/admin/client.ts index 129c4a6..99ae3ae 100644 --- a/src/api/admin/client.ts +++ b/src/api/admin/client.ts @@ -5,16 +5,15 @@ import Client from "../../models/client"; import verify, { Types } from "../middlewares/verify"; import { randomBytes } from "crypto"; - const ClientRouter: Router = Router(); ClientRouter.route("/") /** * @api {get} /admin/client * @apiName AdminGetClients - * + * * @apiGroup admin_client * @apiPermission admin - * + * * @apiSuccess {Object[]} clients * @apiSuccess {String} clients._id The internally used id * @apiSuccess {String} clients.maintainer @@ -26,24 +25,26 @@ ClientRouter.route("/") * @apiSuccess {String} clients.client_id Client ID used outside of DB * @apiSuccess {String} clients.client_secret */ - .get(promiseMiddleware(async (req, res) => { - let clients = await Client.find({}); - //ToDo check if user is required! - res.json(clients); - })) + .get( + promiseMiddleware(async (req, res) => { + let clients = await Client.find({}); + //ToDo check if user is required! + res.json(clients); + }) + ) /** * @api {get} /admin/client * @apiName AdminAddClients - * + * * @apiGroup admin_client * @apiPermission admin - * + * * @apiParam {Boolean} internal Is it an internal app * @apiParam {String} name * @apiParam {String} redirect_url * @apiParam {String} website * @apiParam {String} logo - * + * * @apiSuccess {Object[]} clients * @apiSuccess {String} clients._id The internally used id * @apiSuccess {String} clients.maintainer @@ -55,62 +56,70 @@ ClientRouter.route("/") * @apiSuccess {String} clients.client_id Client ID used outside of DB * @apiSuccess {String} clients.client_secret */ - .post(verify({ - internal: { - type: Types.BOOLEAN, - optional: true - }, - name: { - type: Types.STRING - }, - redirect_url: { - type: Types.STRING - }, - website: { - type: Types.STRING - }, - logo: { - type: Types.STRING, - optional: true - } - }, true), promiseMiddleware(async (req, res) => { - req.body.client_secret = randomBytes(32).toString("hex"); - let client = Client.new(req.body); - client.maintainer = req.user._id; - await Client.save(client) - res.json(client); - })) + .post( + verify( + { + internal: { + type: Types.BOOLEAN, + optional: true, + }, + name: { + type: Types.STRING, + }, + redirect_url: { + type: Types.STRING, + }, + website: { + type: Types.STRING, + }, + logo: { + type: Types.STRING, + optional: true, + }, + }, + true + ), + promiseMiddleware(async (req, res) => { + req.body.client_secret = randomBytes(32).toString("hex"); + let client = Client.new(req.body); + client.maintainer = req.user._id; + await Client.save(client); + res.json(client); + }) + ); ClientRouter.route("/:id") /** * @api {delete} /admin/client/:id * @apiParam {String} id Client _id * @apiName AdminDeleteClient - * + * * @apiGroup admin_client * @apiPermission admin - * + * * @apiSuccess {Boolean} success */ - .delete(promiseMiddleware(async (req, res) => { - let { id } = req.params; - await Client.delete(id); - res.json({ success: true }); - })) + .delete( + promiseMiddleware(async (req, res) => { + let { id } = req.params; + await Client.delete(id); + res.json({ success: true }); + }) + ) /** * @api {put} /admin/client/:id * @apiParam {String} id Client _id * @apiName AdminUpdateClient - * + * * @apiGroup admin_client * @apiPermission admin - * + * * @apiParam {Boolean} internal Is it an internal app * @apiParam {String} name * @apiParam {String} redirect_url * @apiParam {String} website * @apiParam {String} logo - * + * * @apiSuccess {String} _id The internally used id * @apiSuccess {String} maintainer UserID of client maintainer * @apiSuccess {Boolean} internal Defines if it is a internal client @@ -118,40 +127,49 @@ ClientRouter.route("/:id") * @apiSuccess {String} redirect_url Redirect URL after login * @apiSuccess {String} website Website of Client * @apiSuccess {String} logo The Logo of the Client (optional) - * @apiSuccess {String} client_id Client ID used outside of DB + * @apiSuccess {String} client_id Client ID used outside of DB * @apiSuccess {String} client_secret The client secret, that can be used to obtain token */ - .put(verify({ - internal: { - type: Types.BOOLEAN, - optional: true - }, - name: { - type: Types.STRING, - optional: true - }, - redirect_url: { - type: Types.STRING, - optional: true - }, - website: { - type: Types.STRING, - optional: true - }, - logo: { - type: Types.STRING, - optional: true - } - }, true), promiseMiddleware(async (req, res) => { - let { id } = req.query; - let client = await Client.findById(id); - if (!client) throw new RequestError(req.__("Client not found"), HttpStatusCode.BAD_REQUEST); - for (let key in req.body) { - client[key] = req.body[key]; - } - await Client.save(client); - res.json(client); - })) + .put( + verify( + { + internal: { + type: Types.BOOLEAN, + optional: true, + }, + name: { + type: Types.STRING, + optional: true, + }, + redirect_url: { + type: Types.STRING, + optional: true, + }, + website: { + type: Types.STRING, + optional: true, + }, + logo: { + type: Types.STRING, + optional: true, + }, + }, + true + ), + promiseMiddleware(async (req, res) => { + let { id } = req.query; + let client = await Client.findById(id); + if (!client) + throw new RequestError( + req.__("Client not found"), + HttpStatusCode.BAD_REQUEST + ); + for (let key in req.body) { + client[key] = req.body[key]; + } + await Client.save(client); + res.json(client); + }) + ); - -export default ClientRouter; \ No newline at end of file +export default ClientRouter; diff --git a/src/api/admin/index.ts b/src/api/admin/index.ts index b2dadc7..cfc40d2 100644 --- a/src/api/admin/index.ts +++ b/src/api/admin/index.ts @@ -9,12 +9,16 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error"; const AdminRoute: Router = Router(); AdminRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => { - if (!req.isAdmin) throw new RequestError("You have no permission to access this API", HttpStatusCode.FORBIDDEN); - else next() + if (!req.isAdmin) + throw new RequestError( + "You have no permission to access this API", + HttpStatusCode.FORBIDDEN + ); + else next(); }); AdminRoute.use("/client", ClientRoute); -AdminRoute.use("/regcode", RegCodeRoute) -AdminRoute.use("/user", UserRoute) +AdminRoute.use("/regcode", RegCodeRoute); +AdminRoute.use("/user", UserRoute); AdminRoute.use("/permission", PermissionRoute); -export default AdminRoute; \ No newline at end of file +export default AdminRoute; diff --git a/src/api/admin/permission.ts b/src/api/admin/permission.ts index 140111e..5377ce3 100644 --- a/src/api/admin/permission.ts +++ b/src/api/admin/permission.ts @@ -56,18 +56,18 @@ PermissionRoute.route("/") verify( { client: { - type: Types.STRING + type: Types.STRING, }, name: { - type: Types.STRING + type: Types.STRING, }, description: { - type: Types.STRING + type: Types.STRING, }, type: { type: Types.ENUM, - values: ["user", "client"] - } + values: ["user", "client"], + }, }, true ), @@ -83,7 +83,7 @@ PermissionRoute.route("/") description: req.body.description, name: req.body.name, client: client._id, - grant_type: req.body.type + grant_type: req.body.type, }); await Permission.save(permission); res.json(permission); diff --git a/src/api/admin/regcode.ts b/src/api/admin/regcode.ts index e3105ee..b228311 100644 --- a/src/api/admin/regcode.ts +++ b/src/api/admin/regcode.ts @@ -10,54 +10,60 @@ const RegCodeRoute: Router = Router(); RegCodeRoute.route("/") /** * @api {get} /admin/regcode - * @apiName AdminGetRegcodes - * + * @apiName AdminGetRegcodes + * * @apiGroup admin_regcode * @apiPermission admin - * + * * @apiSuccess {Object[]} regcodes * @apiSuccess {String} permissions._id The ID * @apiSuccess {String} permissions.token The Regcode Token * @apiSuccess {String} permissions.valid Defines if the Regcode is valid * @apiSuccess {String} permissions.validTill Expiration date of RegCode */ - .get(promiseMiddleware(async (req, res) => { - let regcodes = await RegCode.find({}); - res.json(regcodes); - })) + .get( + promiseMiddleware(async (req, res) => { + let regcodes = await RegCode.find({}); + res.json(regcodes); + }) + ) /** * @api {delete} /admin/regcode * @apiName AdminDeleteRegcode - * + * * @apiParam {String} id The id of the RegCode - * + * * @apiGroup admin_regcode * @apiPermission admin - * + * * @apiSuccess {Boolean} success */ - .delete(promiseMiddleware(async (req, res) => { - let { id } = req.query; - await RegCode.delete(id); - res.json({ success: true }); - })) + .delete( + promiseMiddleware(async (req, res) => { + let { id } = req.query; + await RegCode.delete(id); + res.json({ success: true }); + }) + ) /** * @api {post} /admin/regcode - * @apiName AdminAddRegcode - * + * @apiName AdminAddRegcode + * * @apiGroup admin_regcode * @apiPermission admin - * + * * @apiSuccess {String} code The newly created code */ - .post(promiseMiddleware(async (req, res) => { - let regcode = RegCode.new({ - token: randomBytes(10).toString("hex"), - valid: true, - validTill: moment().add("1", "month").toDate() + .post( + promiseMiddleware(async (req, res) => { + let regcode = RegCode.new({ + token: randomBytes(10).toString("hex"), + valid: true, + validTill: moment().add("1", "month").toDate(), + }); + await RegCode.save(regcode); + res.json({ code: regcode.token }); }) - await RegCode.save(regcode); - res.json({ code: regcode.token }); - })) + ); -export default RegCodeRoute; \ No newline at end of file +export default RegCodeRoute; diff --git a/src/api/admin/user.ts b/src/api/admin/user.ts index 8c78305..aaaf36e 100644 --- a/src/api/admin/user.ts +++ b/src/api/admin/user.ts @@ -9,15 +9,15 @@ import LoginToken from "../../models/login_token"; const UserRoute: Router = Router(); UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => { - if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN) - else next() -}) + if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN); + else next(); +}); UserRoute.route("/") /** * @api {get} /admin/user - * @apiName AdminGetUsers - * + * @apiName AdminGetUsers + * * @apiGroup admin_user * @apiPermission admin * @apiSuccess {Object[]} user @@ -29,57 +29,65 @@ UserRoute.route("/") * @apiSuccess {Number} user.gender 0 = none, 1 = male, 2 = female, 3 = other * @apiSuccess {Boolean} user.admin Is admin or not */ - .get(promiseMiddleware(async (req, res) => { - let users = await User.find({}); - users.forEach(e => delete e.password && delete e.salt && delete e.encryption_key); - res.json(users); - })) + .get( + promiseMiddleware(async (req, res) => { + let users = await User.find({}); + users.forEach( + (e) => delete e.password && delete e.salt && delete e.encryption_key + ); + res.json(users); + }) + ) /** * @api {delete} /admin/user - * @apiName AdminDeleteUser - * + * @apiName AdminDeleteUser + * * @apiParam {String} id The User ID - * + * * @apiGroup admin_user * @apiPermission admin - * + * * @apiSuccess {Boolean} success */ - .delete(promiseMiddleware(async (req, res) => { - let { id } = req.query; - let user = await User.findById(id); + .delete( + promiseMiddleware(async (req, res) => { + let { id } = req.query; + let user = await User.findById(id); - await Promise.all([ - user.mails.map(mail => Mail.delete(mail)), - [ - RefreshToken.deleteFilter({ user: user._id }), - LoginToken.deleteFilter({ user: user._id }) - ] - ]) + await Promise.all([ + user.mails.map((mail) => Mail.delete(mail)), + [ + RefreshToken.deleteFilter({ user: user._id }), + LoginToken.deleteFilter({ user: user._id }), + ], + ]); - await User.delete(user); - res.json({ success: true }); - })) + await User.delete(user); + res.json({ success: true }); + }) + ) /** * @api {put} /admin/user - * @apiName AdminChangeUser - * + * @apiName AdminChangeUser + * * @apiParam {String} id The User ID - * + * * @apiGroup admin_user * @apiPermission admin - * + * * @apiSuccess {Boolean} success - * - * @apiDescription Flipps the user role: - * admin -> user + * + * @apiDescription Flipps the user role: + * admin -> user * user -> admin */ - .put(promiseMiddleware(async (req, res) => { - let { id } = req.query; - let user = await User.findById(id); - user.admin = !user.admin; - await User.save(user); - res.json({ success: true }) - })) -export default UserRoute; \ No newline at end of file + .put( + promiseMiddleware(async (req, res) => { + let { id } = req.query; + let user = await User.findById(id); + user.admin = !user.admin; + await User.save(user); + res.json({ success: true }); + }) + ); +export default UserRoute; diff --git a/src/api/client/index.ts b/src/api/client/index.ts index bfdd014..29c49de 100644 --- a/src/api/client/index.ts +++ b/src/api/client/index.ts @@ -1,6 +1,9 @@ -import { Request, Response, Router } from "express" +import { Request, Response, Router } from "express"; import Stacker from "../middlewares/stacker"; -import { GetClientAuthMiddleware, GetClientApiAuthMiddleware } from "../middlewares/client"; +import { + GetClientAuthMiddleware, + GetClientApiAuthMiddleware, +} from "../middlewares/client"; import { GetUserMiddleware } from "../middlewares/user"; import { createJWT } from "../../keys"; import Client from "../../models/client"; @@ -8,55 +11,74 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error"; import config from "../../config"; import Mail from "../../models/mail"; - const ClientRouter = Router(); /** * @api {get} /client/user - * + * * @apiDescription Can be used for simple authentication of user. It will redirect the user to the redirect URI with a very short lived jwt. - * + * * @apiParam {String} redirect_uri URL to redirect to on success * @apiParam {String} state A optional state, that will be included in the JWT and redirect_uri as parameter * - * @apiName ClientUser + * @apiName ClientUser * @apiGroup client * * @apiPermission user_client Requires ClientID and Authenticated User */ -ClientRouter.get("/user", Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(false, false), async (req: Request, res: Response) => { - let { redirect_uri, state } = req.query; +ClientRouter.get( + "/user", + Stacker( + GetClientAuthMiddleware(false), + GetUserMiddleware(false, false), + async (req: Request, res: Response) => { + let { redirect_uri, state } = req.query; - if (redirect_uri !== req.client.redirect_url) - throw new RequestError("Invalid redirect URI", HttpStatusCode.BAD_REQUEST); + if (redirect_uri !== req.client.redirect_url) + throw new RequestError( + "Invalid redirect URI", + HttpStatusCode.BAD_REQUEST + ); - let jwt = await createJWT({ - client: req.client.client_id, - uid: req.user.uid, - username: req.user.username, - state: state - }, { - expiresIn: 30, - issuer: config.core.url, - algorithm: "RS256", - subject: req.user.uid, - audience: req.client.client_id - }); //after 30 seconds this token is invalid - res.redirect(redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : "")); -})); - -ClientRouter.get("/account", Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => { - let mails = await Promise.all(req.user.mails.map(id => Mail.findById(id))); - - let mail = mails.find(e => e.primary) || mails[0]; - - res.json({ - user: { - username: req.user.username, - name: req.user.name, - email: mail + let jwt = await createJWT( + { + client: req.client.client_id, + uid: req.user.uid, + username: req.user.username, + state: state, + }, + { + expiresIn: 30, + issuer: config.core.url, + algorithm: "RS256", + subject: req.user.uid, + audience: req.client.client_id, + } + ); //after 30 seconds this token is invalid + res.redirect( + redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : "") + ); } + ) +); + +ClientRouter.get( + "/account", + Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => { + let mails = await Promise.all( + req.user.mails.map((id) => Mail.findById(id)) + ); + + let mail = mails.find((e) => e.primary) || mails[0]; + + res.json({ + user: { + username: req.user.username, + name: req.user.name, + email: mail, + }, + }); }) -})); +); export default ClientRouter; diff --git a/src/api/client/permissions.ts b/src/api/client/permissions.ts index 625cf2f..61a3a78 100644 --- a/src/api/client/permissions.ts +++ b/src/api/client/permissions.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import Stacker from "../middlewares/stacker"; import { ClientAuthMiddleware, - GetClientAuthMiddleware + GetClientAuthMiddleware, } from "../middlewares/client"; import Permission from "../../models/permissions"; import User from "../../models/user"; @@ -22,19 +22,19 @@ export const GetPermissions = Stacker( if (user) { const grant = await Grant.findOne({ client: req.client._id, - user: user + user: user, }); permissions = await Promise.all( - grant.permissions.map(perm => Permission.findById(perm)) - ).then(res => + grant.permissions.map((perm) => Permission.findById(perm)) + ).then((res) => res - .filter(e => e.grant_type === "client") - .map(e => { + .filter((e) => e.grant_type === "client") + .map((e) => { return { id: e._id.toHexString(), name: e.name, - description: e.description + description: e.description, }; }) ); @@ -43,10 +43,10 @@ export const GetPermissions = Stacker( if (permission) { const grants = await Grant.find({ client: req.client._id, - permissions: new ObjectID(permission) + permissions: new ObjectID(permission), }); - users = grants.map(grant => grant.user.toHexString()); + users = grants.map((grant) => grant.user.toHexString()); } res.json({ permissions, users }); @@ -73,14 +73,14 @@ export const PostPermissions = Stacker( let grant = await Grant.findOne({ client: req.client._id, - user: req.user._id + user: req.user._id, }); if (!grant) { grant = Grant.new({ client: req.client._id, user: req.user._id, - permissions: [] + permissions: [], }); } @@ -92,7 +92,7 @@ export const PostPermissions = Stacker( await Grant.save(grant); res.json({ - success: true + success: true, }); } ); diff --git a/src/api/index.ts b/src/api/index.ts index c75032e..1b5767c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,4 @@ -import * as express from "express" +import * as express from "express"; import AdminRoute from "./admin"; import UserRoute from "./user"; import InternalRoute from "./internal"; @@ -9,7 +9,7 @@ import OAuthRoute from "./oauth"; const ApiRouter: express.IRouter = express.Router(); ApiRouter.use("/admin", AdminRoute); -ApiRouter.use(cors()) +ApiRouter.use(cors()); ApiRouter.use("/user", UserRoute); ApiRouter.use("/internal", InternalRoute); ApiRouter.use("/oauth", OAuthRoute); @@ -22,4 +22,4 @@ ApiRouter.use("/", ClientRouter); // Legacy reasons (deprecated) ApiRouter.post("/login", Login); -export default ApiRouter; \ No newline at end of file +export default ApiRouter; diff --git a/src/api/internal/index.ts b/src/api/internal/index.ts index 2d6a515..a857430 100644 --- a/src/api/internal/index.ts +++ b/src/api/internal/index.ts @@ -6,10 +6,10 @@ const InternalRoute: Router = Router(); /** * @api {get} /internal/oauth * @apiName ClientInteralOAuth - * + * * @apiGroup client_internal * @apiPermission client_internal Only ClientID - * + * * @apiParam {String} redirect_uri Redirect URI called after success * @apiParam {String} state State will be set in RedirectURI for the client to check */ @@ -18,13 +18,13 @@ InternalRoute.get("/oauth", OAuthInternalApp); /** * @api {post} /internal/password * @apiName ClientInteralPassword - * + * * @apiGroup client_internal * @apiPermission client_internal Requires ClientID and Secret - * + * * @apiParam {String} username Username (either username or UID) * @apiParam {String} uid User ID (either username or UID) * @apiParam {String} password Hashed and Salted according to specification */ -InternalRoute.post("/password", PasswordAuth) -export default InternalRoute; \ No newline at end of file +InternalRoute.post("/password", PasswordAuth); +export default InternalRoute; diff --git a/src/api/internal/oauth.ts b/src/api/internal/oauth.ts index fc154cb..e123916 100644 --- a/src/api/internal/oauth.ts +++ b/src/api/internal/oauth.ts @@ -6,11 +6,16 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error"; import ClientCode from "../../models/client_code"; import moment = require("moment"); import { randomBytes } from "crypto"; -export const OAuthInternalApp = Stacker(GetClientAuthMiddleware(false, true), UserMiddleware, +export const OAuthInternalApp = Stacker( + GetClientAuthMiddleware(false, true), + UserMiddleware, async (req: Request, res: Response) => { - let { redirect_uri, state } = req.query + let { redirect_uri, state } = req.query; if (!redirect_uri) { - throw new RequestError("No redirect url set!", HttpStatusCode.BAD_REQUEST); + throw new RequestError( + "No redirect url set!", + HttpStatusCode.BAD_REQUEST + ); } let sep = redirect_uri.indexOf("?") < 0 ? "?" : "&"; @@ -20,10 +25,17 @@ export const OAuthInternalApp = Stacker(GetClientAuthMiddleware(false, true), Us client: req.client._id, validTill: moment().add(30, "minutes").toDate(), code: randomBytes(16).toString("hex"), - permissions: [] + permissions: [], }); await ClientCode.save(code); - res.redirect(redirect_uri + sep + "code=" + code.code + (state ? "&state=" + state : "")); + res.redirect( + redirect_uri + + sep + + "code=" + + code.code + + (state ? "&state=" + state : "") + ); res.end(); - }); \ No newline at end of file + } +); diff --git a/src/api/internal/password.ts b/src/api/internal/password.ts index cdc0108..954ae8b 100644 --- a/src/api/internal/password.ts +++ b/src/api/internal/password.ts @@ -4,22 +4,32 @@ import Stacker from "../middlewares/stacker"; import RequestError, { HttpStatusCode } from "../../helper/request_error"; import User from "../../models/user"; -const PasswordAuth = Stacker(GetClientAuthMiddleware(true, true), async (req: Request, res: Response) => { - let { username, password, uid }: { username: string, password: string, uid: string } = req.body; - let query: any = { password: password }; - if (username) { - query.username = username.toLowerCase() - } else if (uid) { - query.uid = uid; - } else { - throw new RequestError(req.__("No username or uid set"), HttpStatusCode.BAD_REQUEST); - } +const PasswordAuth = Stacker( + GetClientAuthMiddleware(true, true), + async (req: Request, res: Response) => { + let { + username, + password, + uid, + }: { username: string; password: string; uid: string } = req.body; + let query: any = { password: password }; + if (username) { + query.username = username.toLowerCase(); + } else if (uid) { + query.uid = uid; + } else { + throw new RequestError( + req.__("No username or uid set"), + HttpStatusCode.BAD_REQUEST + ); + } - let user = await User.findOne(query); - if (!user) { - res.json({ error: req.__("Password or username wrong") }) - } else { - res.json({ success: true, uid: user.uid }); + let user = await User.findOne(query); + if (!user) { + res.json({ error: req.__("Password or username wrong") }); + } else { + res.json({ success: true, uid: user.uid }); + } } -}); -export default PasswordAuth; \ No newline at end of file +); +export default PasswordAuth; diff --git a/src/api/middlewares/client.ts b/src/api/middlewares/client.ts index 85ea437..132f9eb 100644 --- a/src/api/middlewares/client.ts +++ b/src/api/middlewares/client.ts @@ -6,7 +6,11 @@ import User from "../../models/user"; import Mail from "../../models/mail"; import { OAuthJWT } from "../../helper/jwt"; -export function GetClientAuthMiddleware(checksecret = true, internal = false, checksecret_if_available = false) { +export function GetClientAuthMiddleware( + checksecret = true, + internal = false, + checksecret_if_available = false +) { return async (req: Request, res: Response, next: NextFunction) => { try { let client_id = req.query.client_id || req.body.client_id; @@ -24,19 +28,29 @@ export function GetClientAuthMiddleware(checksecret = true, internal = false, ch } if (!client_id || (!client_secret && checksecret)) { - throw new RequestError("No client credentials", HttpStatusCode.BAD_REQUEST); + throw new RequestError( + "No client credentials", + HttpStatusCode.BAD_REQUEST + ); } let w = { client_id: client_id, client_secret: client_secret }; - if (!checksecret && !(checksecret_if_available && client_secret)) delete w.client_secret; + if (!checksecret && !(checksecret_if_available && client_secret)) + delete w.client_secret; - let client = await Client.findOne(w) + let client = await Client.findOne(w); if (!client) { - throw new RequestError("Invalid client_id" + (checksecret ? "or client_secret" : ""), HttpStatusCode.BAD_REQUEST); + throw new RequestError( + "Invalid client_id" + (checksecret ? "or client_secret" : ""), + HttpStatusCode.BAD_REQUEST + ); } if (internal && !client.internal) { - throw new RequestError(req.__("Client has no permission for access"), HttpStatusCode.FORBIDDEN) + throw new RequestError( + req.__("Client has no permission for access"), + HttpStatusCode.FORBIDDEN + ); } req.client = client; next(); @@ -44,7 +58,7 @@ export function GetClientAuthMiddleware(checksecret = true, internal = false, ch if (next) next(e); else throw e; } - } + }; } export const ClientAuthMiddleware = GetClientAuthMiddleware(); @@ -52,10 +66,13 @@ export const ClientAuthMiddleware = GetClientAuthMiddleware(); export function GetClientApiAuthMiddleware(permissions?: string[]) { return async (req: Request, res: Response, next: NextFunction) => { try { - const invalid_err = new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED); - let token: string = req.query.access_token || req.headers.authorization; - if (!token) - throw invalid_err; + const invalid_err = new RequestError( + req.__("You are not logged in or your login is expired"), + HttpStatusCode.UNAUTHORIZED + ); + let token: string = + req.query.access_token || req.headers.authorization; + if (!token) throw invalid_err; if (token.toLowerCase().startsWith("bearer ")) token = token.substring(7); @@ -64,19 +81,21 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) { try { data = await validateJWT(token); } catch (err) { - throw invalid_err + throw invalid_err; } let user = await User.findOne({ uid: data.user }); - if (!user) - throw invalid_err; + if (!user) throw invalid_err; - let client = await Client.findOne({ client_id: data.application }) - if (!client) - throw invalid_err; + let client = await Client.findOne({ client_id: data.application }); + if (!client) throw invalid_err; - if (permissions && (!data.permissions || !permissions.every(e => data.permissions.indexOf(e) >= 0))) + if ( + permissions && + (!data.permissions || + !permissions.every((e) => data.permissions.indexOf(e) >= 0)) + ) throw invalid_err; req.user = user; @@ -86,5 +105,5 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) { if (next) next(e); else throw e; } - } + }; } diff --git a/src/api/middlewares/stacker.ts b/src/api/middlewares/stacker.ts index 1c0d498..47369a5 100644 --- a/src/api/middlewares/stacker.ts +++ b/src/api/middlewares/stacker.ts @@ -8,19 +8,21 @@ function call(handler: RH, req: Request, res: Response) { let p = handler(req, res, (err) => { if (err) no(err); else yes(); - }) - if (p && p.catch) p.catch(err => no(err)); - }) + }); + if (p && p.catch) p.catch((err) => no(err)); + }); } const Stacker = (...handler: RH[]) => { - return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => { - let hc = handler.concat(); - while (hc.length > 0) { - let h = hc.shift(); - await call(h, req, res); + return promiseMiddleware( + async (req: Request, res: Response, next: NextFunction) => { + let hc = handler.concat(); + while (hc.length > 0) { + let h = hc.shift(); + await call(h, req, res); + } + next(); } - next(); - }); -} -export default Stacker; \ No newline at end of file + ); +}; +export default Stacker; diff --git a/src/api/middlewares/user.ts b/src/api/middlewares/user.ts index 9c4e5bc..dcbc575 100644 --- a/src/api/middlewares/user.ts +++ b/src/api/middlewares/user.ts @@ -22,7 +22,7 @@ export function GetUserMiddleware( redirect_uri?: string, validated = true ) { - return promiseMiddleware(async function( + return promiseMiddleware(async function ( req: Request, res: Response, next?: NextFunction @@ -57,7 +57,7 @@ export function GetUserMiddleware( token: special, special: true, valid: true, - user: token.user + user: token.user, }); if (!(await CheckToken(special_token, validated))) invalid("Special token invalid"); @@ -68,7 +68,7 @@ export function GetUserMiddleware( req.isAdmin = user.admin; req.token = { login: token, - special: special_token + special: special_token, }; if (next) next(); diff --git a/src/api/middlewares/verify.ts b/src/api/middlewares/verify.ts index aa93542..8bd1ee0 100644 --- a/src/api/middlewares/verify.ts +++ b/src/api/middlewares/verify.ts @@ -1,6 +1,14 @@ -import { Request, Response, NextFunction } from "express" +import { Request, Response, NextFunction } from "express"; import { Logging } from "@hibas123/nodelogging"; -import { isBoolean, isString, isNumber, isObject, isDate, isArray, isSymbol } from "util"; +import { + isBoolean, + isString, + isNumber, + isObject, + isDate, + isArray, + isSymbol, +} from "util"; import RequestError, { HttpStatusCode } from "../../helper/request_error"; export enum Types { @@ -11,39 +19,41 @@ export enum Types { OBJECT, DATE, ARRAY, - ENUM + ENUM, } function isEmail(value: any): boolean { - return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value) + return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ); } export interface CheckObject { - type: Types - query?: boolean - optional?: boolean + type: Types; + query?: boolean; + optional?: boolean; /** * Only when Type.ENUM - * + * * values to check before */ - values?: string[] + values?: string[]; /** * Only when Type.STRING */ - notempty?: boolean // Only STRING + notempty?: boolean; // Only STRING } export interface Checks { - [index: string]: CheckObject// | Types + [index: string]: CheckObject; // | Types } // req: Request, res: Response, next: NextFunction export default function (fields: Checks, noadditional = false) { return (req: Request, res: Response, next: NextFunction) => { - let errors: { message: string, field: string }[] = [] + let errors: { message: string; field: string }[] = []; function check(data: any, field_name: string, field: CheckObject) { if (data !== undefined && data !== null) { @@ -78,48 +88,60 @@ export default function (fields: Checks, noadditional = false) { } break; default: - Logging.error(`Invalid type to check: ${field.type} ${Types[field.type]}`) + Logging.error( + `Invalid type to check: ${field.type} ${Types[field.type]}` + ); } errors.push({ - message: res.__("Field {{field}} has wrong type. It should be from type {{type}}", { field: field_name, type: Types[field.type].toLowerCase() }), - field: field_name - }) + message: res.__( + "Field {{field}} has wrong type. It should be from type {{type}}", + { field: field_name, type: Types[field.type].toLowerCase() } + ), + field: field_name, + }); } else { - if (!field.optional) errors.push({ - message: res.__("Field {{field}} is not defined", { field: field_name }), - field: field_name - }) + if (!field.optional) + errors.push({ + message: res.__("Field {{field}} is not defined", { + field: field_name, + }), + field: field_name, + }); } } for (let field_name in fields) { - let field = fields[field_name] - let data = fields[field_name].query ? req.query[field_name] : req.body[field_name] - check(data, field_name, field) + let field = fields[field_name]; + let data = fields[field_name].query + ? req.query[field_name] + : req.body[field_name]; + check(data, field_name, field); } - if (noadditional) { //Checks if the data given has additional parameters + if (noadditional) { + //Checks if the data given has additional parameters let should = Object.keys(fields); - should = should.filter(e => !fields[e].query); //Query parameters should not exist on body + should = should.filter((e) => !fields[e].query); //Query parameters should not exist on body let has = Object.keys(req.body); - has.every(e => { + has.every((e) => { if (should.indexOf(e) >= 0) { return true; } else { errors.push({ - message: res.__("Field {{field}} should not be there", { field: e }), - field: e - }) + message: res.__("Field {{field}} should not be there", { + field: e, + }), + field: e, + }); return false; } - }) + }); } if (errors.length > 0) { let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true); next(err); - } else - next() - } -} \ No newline at end of file + } else next(); + }; +} diff --git a/src/api/oauth/auth.ts b/src/api/oauth/auth.ts index d8370f0..e5efa98 100644 --- a/src/api/oauth/auth.ts +++ b/src/api/oauth/auth.ts @@ -93,11 +93,13 @@ const GetAuthRoute = (view = false) => redirect_uri, scope, state, - nored + nored, } = req.query; - const sendError = type => { + const sendError = (type) => { if (redirect_uri === "$local") redirect_uri = "/code"; - res.redirect((redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)); + res.redirect( + (redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`) + ); }; const scopes = scope.split(";"); @@ -123,7 +125,7 @@ const GetAuthRoute = (view = false) => let permissions: IPermission[] = []; let proms: PromiseLike[] = []; if (scopes) { - for (let perm of scopes.filter(e => e !== "read_user")) { + for (let perm of scopes.filter((e) => e !== "read_user")) { let oid = undefined; try { oid = new ObjectID(perm); @@ -132,7 +134,7 @@ const GetAuthRoute = (view = false) => continue; } proms.push( - Permission.findById(oid).then(p => { + Permission.findById(oid).then((p) => { if (!p) return Promise.reject(new Error()); permissions.push(p); }) @@ -141,7 +143,7 @@ const GetAuthRoute = (view = false) => } let err = undefined; - await Promise.all(proms).catch(e => { + await Promise.all(proms).catch((e) => { err = e; }); @@ -152,7 +154,7 @@ const GetAuthRoute = (view = false) => let grant: IGrant | undefined = await Grant.findOne({ client: client._id, - user: req.user._id + user: req.user._id, }); Logging.debug("Grant", grant, permissions); @@ -161,14 +163,14 @@ const GetAuthRoute = (view = false) => if (grant) { missing_permissions = grant.permissions - .map(perm => permissions.find(p => p._id.equals(perm))) - .filter(e => !!e); + .map((perm) => permissions.find((p) => p._id.equals(perm))) + .filter((e) => !!e); } else { missing_permissions = permissions; } let client_granted_perm = missing_permissions.filter( - e => e.grant_type == "client" + (e) => e.grant_type == "client" ); if (client_granted_perm.length > 0) { return sendError("no_permission"); @@ -186,11 +188,11 @@ const GetAuthRoute = (view = false) => GetAuthPage( req.__, client.name, - permissions.map(perm => { + permissions.map((perm) => { return { name: perm.name, description: perm.description, - logo: client.logo + logo: client.logo, }; }) ) @@ -202,11 +204,11 @@ const GetAuthRoute = (view = false) => grant = Grant.new({ client: client._id, user: req.user._id, - permissions: [] + permissions: [], }); grant.permissions.push( - ...missing_permissions.map(e => e._id) + ...missing_permissions.map((e) => e._id) ); await Grant.save(grant); } else { @@ -218,20 +220,19 @@ const GetAuthRoute = (view = false) => let code = ClientCode.new({ user: req.user._id, client: client._id, - permissions: permissions.map(p => p._id), - validTill: moment() - .add(30, "minutes") - .toDate(), - code: randomBytes(16).toString("hex") + permissions: permissions.map((p) => p._id), + validTill: moment().add(30, "minutes").toDate(), + code: randomBytes(16).toString("hex"), }); await ClientCode.save(code); let redir = client.redirect_url === "$local" ? "/code" : client.redirect_url; - let ruri = redir + `?code=${code.code}${state ? "&state=" + state : ""}`; + let ruri = + redir + `?code=${code.code}${state ? "&state=" + state : ""}`; if (nored === "true") { res.json({ - redirect_uri: ruri + redirect_uri: ruri, }); } else { res.redirect(ruri); diff --git a/src/api/oauth/index.ts b/src/api/oauth/index.ts index 16ae123..a68bad6 100644 --- a/src/api/oauth/index.ts +++ b/src/api/oauth/index.ts @@ -8,10 +8,10 @@ const OAuthRoue: Router = Router(); /** * @api {post} /oauth/auth * @apiName OAuthAuth - * + * * @apiGroup oauth * @apiPermission user Special required - * + * * @apiParam {String} response_type must be "code" others are not supported * @apiParam {String} client_id ClientID * @apiParam {String} redirect_uri The URI to redirect with code @@ -24,31 +24,31 @@ OAuthRoue.post("/auth", GetAuthRoute(false)); /** * @api {get} /oauth/jwt * @apiName OAuthJwt - * + * * @apiGroup oauth * @apiPermission none - * + * * @apiParam {String} refreshtoken - * + * * @apiSuccess {String} token The JWT that allowes the application to access the recources granted for refresh token */ -OAuthRoue.get("/jwt", JWTRoute) +OAuthRoue.get("/jwt", JWTRoute); /** * @api {get} /oauth/public * @apiName OAuthPublic - * + * * @apiGroup oauth * @apiPermission none - * + * * @apiSuccess {String} public_key The applications public_key. Used to verify JWT. */ -OAuthRoue.get("/public", Public) +OAuthRoue.get("/public", Public); /** * @api {get} /oauth/refresh * @apiName OAuthRefreshGet - * + * * @apiGroup oauth */ OAuthRoue.get("/refresh", RefreshTokenRoute); @@ -56,7 +56,7 @@ OAuthRoue.get("/refresh", RefreshTokenRoute); /** * @api {post} /oauth/refresh * @apiName OAuthRefreshPost - * + * * @apiGroup oauth */ OAuthRoue.post("/refresh", RefreshTokenRoute); diff --git a/src/api/oauth/jwt.ts b/src/api/oauth/jwt.ts index edc248e..2199660 100644 --- a/src/api/oauth/jwt.ts +++ b/src/api/oauth/jwt.ts @@ -8,21 +8,36 @@ import { getAccessTokenJWT } from "../../helper/jwt"; const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => { let { refreshtoken } = req.query; - if (!refreshtoken) throw new RequestError(req.__("Refresh token not set"), HttpStatusCode.BAD_REQUEST); + if (!refreshtoken) + throw new RequestError( + req.__("Refresh token not set"), + HttpStatusCode.BAD_REQUEST + ); let token = await RefreshToken.findOne({ token: refreshtoken }); - if (!token) throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST); + if (!token) + throw new RequestError( + req.__("Invalid token"), + HttpStatusCode.BAD_REQUEST + ); let user = await User.findById(token.user); if (!user) { token.valid = false; await RefreshToken.save(token); - throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST); + throw new RequestError( + req.__("Invalid token"), + HttpStatusCode.BAD_REQUEST + ); } let client = await Client.findById(token.client); - let jwt = await getAccessTokenJWT({ user, permissions: token.permissions, client }); + let jwt = await getAccessTokenJWT({ + user, + permissions: token.permissions, + client, + }); res.json({ token: jwt }); -}) -export default JWTRoute; \ No newline at end of file +}); +export default JWTRoute; diff --git a/src/api/oauth/public.ts b/src/api/oauth/public.ts index 33c204c..1534afa 100644 --- a/src/api/oauth/public.ts +++ b/src/api/oauth/public.ts @@ -2,5 +2,5 @@ import { Request, Response } from "express"; import { public_key } from "../../keys"; export default function Public(req: Request, res: Response) { - res.json({ public_key: public_key }) -} \ No newline at end of file + res.json({ public_key: public_key }); +} diff --git a/src/api/oauth/refresh.ts b/src/api/oauth/refresh.ts index 9212e5e..4357b28 100644 --- a/src/api/oauth/refresh.ts +++ b/src/api/oauth/refresh.ts @@ -2,9 +2,13 @@ import { Request, Response } from "express"; import RequestError, { HttpStatusCode } from "../../helper/request_error"; import User from "../../models/user"; import Client from "../../models/client"; -import { getAccessTokenJWT, getIDToken, AccessTokenJWTExp } from "../../helper/jwt"; +import { + getAccessTokenJWT, + getIDToken, + AccessTokenJWTExp, +} from "../../helper/jwt"; import Stacker from "../middlewares/stacker"; -import { GetClientAuthMiddleware } from "../middlewares/client" +import { GetClientAuthMiddleware } from "../middlewares/client"; import ClientCode from "../../models/client_code"; import Mail from "../../models/mail"; import { randomBytes } from "crypto"; @@ -25,72 +29,95 @@ token, which will inform the authorization server of the breach. const refreshTokenValidTime = moment.duration(6, "month"); -const RefreshTokenRoute = Stacker(GetClientAuthMiddleware(false, false, true), async (req: Request, res: Response) => { - let grant_type = req.query.grant_type || req.body.grant_type; - if (!grant_type || grant_type === "authorization_code") { - let code = req.query.code || req.body.code; - let nonce = req.query.nonce || req.body.nonce; +const RefreshTokenRoute = Stacker( + GetClientAuthMiddleware(false, false, true), + async (req: Request, res: Response) => { + let grant_type = req.query.grant_type || req.body.grant_type; + if (!grant_type || grant_type === "authorization_code") { + let code = req.query.code || req.body.code; + let nonce = req.query.nonce || req.body.nonce; - let c = await ClientCode.findOne({ code: code }) - if (!c || moment(c.validTill).isBefore()) { - throw new RequestError(req.__("Invalid code"), HttpStatusCode.BAD_REQUEST); + let c = await ClientCode.findOne({ code: code }); + if (!c || moment(c.validTill).isBefore()) { + throw new RequestError( + req.__("Invalid code"), + HttpStatusCode.BAD_REQUEST + ); + } + + let client = await Client.findById(c.client); + + let user = await User.findById(c.user); + let mails = await Promise.all(user.mails.map((m) => Mail.findOne(m))); + + let token = RefreshToken.new({ + user: c.user, + client: c.client, + permissions: c.permissions, + token: randomBytes(16).toString("hex"), + valid: true, + validTill: moment().add(refreshTokenValidTime).toDate(), + }); + await RefreshToken.save(token); + await ClientCode.delete(c); + + let mail = mails.find((e) => e.primary); + if (!mail) mail = mails[0]; + + res.json({ + refresh_token: token.token, + token: token.token, + access_token: await getAccessTokenJWT({ + client: client, + user: user, + permissions: c.permissions, + }), + token_type: "bearer", + expires_in: AccessTokenJWTExp.asSeconds(), + profile: { + uid: user.uid, + email: mail ? mail.mail : "", + name: user.name, + enc_key: getEncryptionKey(user, client), + }, + id_token: getIDToken(user, client.client_id, nonce), + }); + } else if (grant_type === "refresh_token") { + let refresh_token = req.query.refresh_token || req.body.refresh_token; + if (!refresh_token) + throw new RequestError( + req.__("refresh_token not set"), + HttpStatusCode.BAD_REQUEST + ); + + let token = await RefreshToken.findOne({ token: refresh_token }); + if (!token || !token.valid || moment(token.validTill).isBefore()) + throw new RequestError( + req.__("Invalid token"), + HttpStatusCode.BAD_REQUEST + ); + + token.validTill = moment().add(refreshTokenValidTime).toDate(); + await RefreshToken.save(token); + + let user = await User.findById(token.user); + let client = await Client.findById(token.client); + let jwt = await getAccessTokenJWT({ + user, + client, + permissions: token.permissions, + }); + res.json({ + access_token: jwt, + expires_in: AccessTokenJWTExp.asSeconds(), + }); + } else { + throw new RequestError( + "invalid grant_type", + HttpStatusCode.BAD_REQUEST + ); } - - let client = await Client.findById(c.client); - - let user = await User.findById(c.user); - let mails = await Promise.all(user.mails.map(m => Mail.findOne(m))); - - let token = RefreshToken.new({ - user: c.user, - client: c.client, - permissions: c.permissions, - token: randomBytes(16).toString("hex"), - valid: true, - validTill: moment().add(refreshTokenValidTime).toDate() - }); - await RefreshToken.save(token); - await ClientCode.delete(c); - - let mail = mails.find(e => e.primary); - if (!mail) mail = mails[0]; - - res.json({ - refresh_token: token.token, - token: token.token, - access_token: await getAccessTokenJWT({ - client: client, - user: user, - permissions: c.permissions - }), - token_type: "bearer", - expires_in: AccessTokenJWTExp.asSeconds(), - profile: { - uid: user.uid, - email: mail ? mail.mail : "", - name: user.name, - enc_key: getEncryptionKey(user, client) - }, - id_token: getIDToken(user, client.client_id, nonce) - }); - } else if (grant_type === "refresh_token") { - let refresh_token = req.query.refresh_token || req.body.refresh_token; - if (!refresh_token) throw new RequestError(req.__("refresh_token not set"), HttpStatusCode.BAD_REQUEST); - - let token = await RefreshToken.findOne({ token: refresh_token }); - if (!token || !token.valid || moment(token.validTill).isBefore()) - throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST); - - token.validTill = moment().add(refreshTokenValidTime).toDate(); - await RefreshToken.save(token); - - let user = await User.findById(token.user); - let client = await Client.findById(token.client) - let jwt = await getAccessTokenJWT({ user, client, permissions: token.permissions }); - res.json({ access_token: jwt, expires_in: AccessTokenJWTExp.asSeconds() }); - } else { - throw new RequestError("invalid grant_type", HttpStatusCode.BAD_REQUEST); } -}) +); -export default RefreshTokenRoute; \ No newline at end of file +export default RefreshTokenRoute; diff --git a/src/api/user/account.ts b/src/api/user/account.ts index 6dfb385..bd49531 100644 --- a/src/api/user/account.ts +++ b/src/api/user/account.ts @@ -4,13 +4,16 @@ import { GetUserMiddleware } from "../middlewares/user"; import LoginToken, { CheckToken } from "../../models/login_token"; import RequestError, { HttpStatusCode } from "../../helper/request_error"; -export const GetAccount = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { - let user = { - id: req.user.uid, - name: req.user.name, - username: req.user.username, - birthday: req.user.birthday, - gender: req.user.gender, - }; - res.json({ user }); -}); +export const GetAccount = Stacker( + GetUserMiddleware(true, true), + async (req: Request, res: Response) => { + let user = { + id: req.user.uid, + name: req.user.name, + username: req.user.username, + birthday: req.user.birthday, + gender: req.user.gender, + }; + res.json({ user }); + } +); diff --git a/src/api/user/contact.ts b/src/api/user/contact.ts index f313c58..ad67a17 100644 --- a/src/api/user/contact.ts +++ b/src/api/user/contact.ts @@ -3,14 +3,17 @@ import Stacker from "../middlewares/stacker"; import { GetUserMiddleware } from "../middlewares/user"; import Mail from "../../models/mail"; -export const GetContactInfos = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { - let mails = await Promise.all( - req.user.mails.map(mail => Mail.findById(mail)) - ); +export const GetContactInfos = Stacker( + GetUserMiddleware(true, true), + async (req: Request, res: Response) => { + let mails = await Promise.all( + req.user.mails.map((mail) => Mail.findById(mail)) + ); - let contact = { - mails: mails.filter(e => !!e), - phones: req.user.phones - }; - res.json({ contact }); -}); + let contact = { + mails: mails.filter((e) => !!e), + phones: req.user.phones, + }; + res.json({ contact }); + } +); diff --git a/src/api/user/index.ts b/src/api/user/index.ts index 900b4e2..2df9f0c 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -41,20 +41,20 @@ UserRoute.post("/register", Register); /** * @api {post} /user/login?type=:type * @apiName UserLogin - * + * * @apiParam {String} type Type could be either "username" or "password" - * + * * @apiGroup user * @apiPermission none - * + * * @apiParam {String} username Username (either username or uid required) * @apiParam {String} uid (either username or uid required) * @apiParam {String} password Password hashed and salted like specification (only on type password) * @apiParam {Number} time in milliseconds used to hash password. This is used to make passwords "expire" - * + * * @apiSuccess {String} uid On type = "username" * @apiSuccess {String} salt On type = "username" - * + * * @apiSuccess {String} login On type = "password". Login Token * @apiSuccess {String} special On type = "password". Special Token * @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required @@ -62,16 +62,16 @@ UserRoute.post("/register", Register); * @apiSuccess {String} tfa.name The name of the TFA Method * @apiSuccess {String} tfa.type The type of the TFA Method */ -UserRoute.post("/login", Login) +UserRoute.post("/login", Login); UserRoute.use("/twofactor", TwoFactorRoute); /** * @api {get} /user/token * @apiName UserGetToken - * + * * @apiGroup user * @apiPermission user - * + * * @apiSuccess {Object[]} token * @apiSuccess {String} token.id The Token ID * @apiSuccess {String} token.special Identifies Special Token @@ -84,25 +84,24 @@ UserRoute.get("/token", GetToken); /** * @api {delete} /user/token/:id * @apiParam {String} id The id of the token to be deleted - * + * * @apiName UserDeleteToken - * - * + * + * * @apiGroup user * @apiPermission user - * + * * @apiSuccess {Boolean} success */ UserRoute.delete("/token/:id", DeleteToken); - /** * @api {delete} /user/account * @apiName UserGetAccount - * + * * @apiGroup user * @apiPermission user - * + * * @apiSuccess {Boolean} success * @apiSuccess {Object[]} user * @apiSuccess {String} user.id User ID @@ -126,4 +125,4 @@ UserRoute.get("/account", GetAccount); * @apiSuccess {Object[]} user.phone Phone numbers */ UserRoute.get("/contact", GetContactInfos); -export default UserRoute; \ No newline at end of file +export default UserRoute; diff --git a/src/api/user/login.ts b/src/api/user/login.ts index d1e36fa..edce04e 100644 --- a/src/api/user/login.ts +++ b/src/api/user/login.ts @@ -1,4 +1,4 @@ -import { Request, Response } from "express" +import { Request, Response } from "express"; import User, { IUser } from "../../models/user"; import { randomBytes } from "crypto"; import moment = require("moment"); @@ -12,36 +12,39 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => { let type = req.query.type; if (type === "username") { let { username, uid } = req.query; - let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid }); + let user = await User.findOne( + username ? { username: username.toLowerCase() } : { uid: uid } + ); if (!user) { - res.json({ error: req.__("User not found") }) + res.json({ error: req.__("User not found") }); } else { res.json({ salt: user.salt, uid: user.uid }); } return; } else if (type === "password") { const sendToken = async (user: IUser, tfa?: any[]) => { - let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress + let ip = + req.headers["x-forwarded-for"] || req.connection.remoteAddress; let client = { ip: Array.isArray(ip) ? ip[0] : ip, - browser: req.headers["user-agent"] - } + browser: req.headers["user-agent"], + }; let token_str = randomBytes(16).toString("hex"); - let tfa_exp = moment().add(5, "minutes").toDate() - let token_exp = moment().add(6, "months").toDate() + let tfa_exp = moment().add(5, "minutes").toDate(); + let token_exp = moment().add(6, "months").toDate(); let token = LoginToken.new({ token: token_str, valid: true, validTill: tfa ? tfa_exp : token_exp, user: user._id, validated: tfa ? false : true, - ...client + ...client, }); await LoginToken.save(token); let special_str = randomBytes(24).toString("hex"); - let special_exp = moment().add(30, "minutes").toDate() + let special_exp = moment().add(30, "minutes").toDate(); let special = LoginToken.new({ token: special_str, valid: true, @@ -49,50 +52,74 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => { special: true, user: user._id, validated: tfa ? false : true, - ...client + ...client, }); await LoginToken.save(special); res.json({ login: { token: token_str, expires: token.validTill.toUTCString() }, - special: { token: special_str, expires: special.validTill.toUTCString() }, - tfa + special: { + token: special_str, + expires: special.validTill.toUTCString(), + }, + tfa, }); - } + }; let { username, password, uid, date } = req.body; - let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid }) + let user = await User.findOne( + username ? { username: username.toLowerCase() } : { uid: uid } + ); if (!user) { - res.json({ error: req.__("User not found") }) + res.json({ error: req.__("User not found") }); } else { let upw = user.password; if (date) { - if (!moment(date).isBetween(moment().subtract(1, "minute"), moment().add(1, "minute"))) { - res.json({ error: req.__("Invalid timestamp. Please check your devices time!") }); + if ( + !moment(date).isBetween( + moment().subtract(1, "minute"), + moment().add(1, "minute") + ) + ) { + res.json({ + error: req.__( + "Invalid timestamp. Please check your devices time!" + ), + }); return; } else { - upw = crypto.createHash("sha512").update(upw + date.toString()).digest("hex"); + upw = crypto + .createHash("sha512") + .update(upw + date.toString()) + .digest("hex"); } } if (upw !== password) { - res.json({ error: req.__("Password or username wrong") }) + res.json({ error: req.__("Password or username wrong") }); } else { - let twofactor = await TwoFactor.find({ user: user._id, valid: true }) - let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false) - await Promise.all(expired.map(e => { - e.valid = false; - return TwoFactor.save(e); - })); - twofactor = twofactor.filter(e => e.valid); + let twofactor = await TwoFactor.find({ + user: user._id, + valid: true, + }); + let expired = twofactor.filter((e) => + e.expires ? moment().isAfter(moment(e.expires)) : false + ); + await Promise.all( + expired.map((e) => { + e.valid = false; + return TwoFactor.save(e); + }) + ); + twofactor = twofactor.filter((e) => e.valid); if (twofactor && twofactor.length > 0) { - let tfa = twofactor.map(e => { + let tfa = twofactor.map((e) => { return { id: e._id, name: e.name || TFANames.get(e.type), - type: e.type - } - }) + type: e.type, + }; + }); await sendToken(user, tfa); } else { await sendToken(user); @@ -104,4 +131,4 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => { } }); -export default Login; \ No newline at end of file +export default Login; diff --git a/src/api/user/register.ts b/src/api/user/register.ts index 04f7d44..07c744d 100644 --- a/src/api/user/register.ts +++ b/src/api/user/register.ts @@ -1,4 +1,4 @@ -import { Request, Response, Router } from "express" +import { Request, Response, Router } from "express"; import Stacker from "../middlewares/stacker"; import verify, { Types } from "../middlewares/verify"; import promiseMiddleware from "../../helper/promiseMiddleware"; @@ -7,138 +7,149 @@ import { HttpStatusCode } from "../../helper/request_error"; import Mail from "../../models/mail"; import RegCode from "../../models/regcodes"; -const Register = Stacker(verify({ - mail: { - type: Types.EMAIL, - notempty: true - }, - username: { - type: Types.STRING, - notempty: true - }, - password: { - type: Types.STRING, - notempty: true - }, - salt: { - type: Types.STRING, - notempty: true - }, - regcode: { - type: Types.STRING, - notempty: true - }, - gender: { - type: Types.STRING, - notempty: true - }, - name: { - type: Types.STRING, - notempty: true - }, - // birthday: { - // type: Types.DATE - // } -}), promiseMiddleware(async (req: Request, res: Response) => { - let { username, password, salt, mail, gender, name, birthday, regcode } = req.body; - let u = await User.findOne({ username: username.toLowerCase() }) - if (u) { - let err = { - message: [ - { - message: req.__("Username taken"), - field: "username" - } - ], - status: HttpStatusCode.BAD_REQUEST, - nolog: true +const Register = Stacker( + verify({ + mail: { + type: Types.EMAIL, + notempty: true, + }, + username: { + type: Types.STRING, + notempty: true, + }, + password: { + type: Types.STRING, + notempty: true, + }, + salt: { + type: Types.STRING, + notempty: true, + }, + regcode: { + type: Types.STRING, + notempty: true, + }, + gender: { + type: Types.STRING, + notempty: true, + }, + name: { + type: Types.STRING, + notempty: true, + }, + // birthday: { + // type: Types.DATE + // } + }), + promiseMiddleware(async (req: Request, res: Response) => { + let { + username, + password, + salt, + mail, + gender, + name, + birthday, + regcode, + } = req.body; + let u = await User.findOne({ username: username.toLowerCase() }); + if (u) { + let err = { + message: [ + { + message: req.__("Username taken"), + field: "username", + }, + ], + status: HttpStatusCode.BAD_REQUEST, + nolog: true, + }; + throw err; } - throw err; - } - - let m = await Mail.findOne({ mail: mail }) - if (m) { - let err = { - message: [ - { - message: req.__("Mail linked with other account"), - field: "mail" - } - ], - status: HttpStatusCode.BAD_REQUEST, - nolog: true + let m = await Mail.findOne({ mail: mail }); + if (m) { + let err = { + message: [ + { + message: req.__("Mail linked with other account"), + field: "mail", + }, + ], + status: HttpStatusCode.BAD_REQUEST, + nolog: true, + }; + throw err; } - throw err; - } - let regc = await RegCode.findOne({ token: regcode }) - if (!regc) { - let err = { - message: [ - { - message: req.__("Invalid registration code"), - field: "regcode" - } - ], - status: HttpStatusCode.BAD_REQUEST, - nolog: true + let regc = await RegCode.findOne({ token: regcode }); + if (!regc) { + let err = { + message: [ + { + message: req.__("Invalid registration code"), + field: "regcode", + }, + ], + status: HttpStatusCode.BAD_REQUEST, + nolog: true, + }; + throw err; } - throw err; - } - if (!regc.valid) { - let err = { - message: [ - { - message: req.__("Registration code already used"), - field: "regcode" - } - ], - status: HttpStatusCode.BAD_REQUEST, - nolog: true + if (!regc.valid) { + let err = { + message: [ + { + message: req.__("Registration code already used"), + field: "regcode", + }, + ], + status: HttpStatusCode.BAD_REQUEST, + nolog: true, + }; + throw err; } - throw err; - } - let g = -1; - switch (gender) { - case "male": - g = Gender.male - break; - case "female": - g = Gender.female - break; - case "other": - g = Gender.other - break; - default: - g = Gender.none - break; - } + let g = -1; + switch (gender) { + case "male": + g = Gender.male; + break; + case "female": + g = Gender.female; + break; + case "other": + g = Gender.other; + break; + default: + g = Gender.none; + break; + } - let user = User.new({ - username: username.toLowerCase(), - password: password, - salt: salt, - gender: g, - name: name, - // birthday: birthday, - admin: false + let user = User.new({ + username: username.toLowerCase(), + password: password, + salt: salt, + gender: g, + name: name, + // birthday: birthday, + admin: false, + }); + + regc.valid = false; + await RegCode.save(regc); + + let ml = Mail.new({ + mail: mail, + primary: true, + }); + + await Mail.save(ml); + + user.mails.push(ml._id); + await User.save(user); + res.json({ success: true }); }) - - regc.valid = false; - await RegCode.save(regc); - - let ml = Mail.new({ - mail: mail, - primary: true - }) - - await Mail.save(ml); - - user.mails.push(ml._id); - await User.save(user) - res.json({ success: true }); -})) -export default Register; \ No newline at end of file +); +export default Register; diff --git a/src/api/user/token.ts b/src/api/user/token.ts index 76dc47b..94dc6be 100644 --- a/src/api/user/token.ts +++ b/src/api/user/token.ts @@ -4,26 +4,42 @@ import { GetUserMiddleware } from "../middlewares/user"; import LoginToken, { CheckToken } from "../../models/login_token"; import RequestError, { HttpStatusCode } from "../../helper/request_error"; -export const GetToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { - let raw_token = await LoginToken.find({ user: req.user._id, valid: true }); - let token = await Promise.all(raw_token.map(async token => { - await CheckToken(token); - return { - id: token._id, - special: token.special, - ip: token.ip, - browser: token.browser, - isthis: token._id.equals(token.special ? req.token.special._id : req.token.login._id) - } - }).filter(t => t !== undefined)); - res.json({ token }); -}); +export const GetToken = Stacker( + GetUserMiddleware(true, true), + async (req: Request, res: Response) => { + let raw_token = await LoginToken.find({ + user: req.user._id, + valid: true, + }); + let token = await Promise.all( + raw_token + .map(async (token) => { + await CheckToken(token); + return { + id: token._id, + special: token.special, + ip: token.ip, + browser: token.browser, + isthis: token._id.equals( + token.special ? req.token.special._id : req.token.login._id + ), + }; + }) + .filter((t) => t !== undefined) + ); + res.json({ token }); + } +); -export const DeleteToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { - let { id } = req.params; - let token = await LoginToken.findById(id); - if (!token || !token.user.equals(req.user._id)) throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST); - token.valid = false; - await LoginToken.save(token); - res.json({ success: true }); -}); \ No newline at end of file +export const DeleteToken = Stacker( + GetUserMiddleware(true, true), + async (req: Request, res: Response) => { + let { id } = req.params; + let token = await LoginToken.findById(id); + if (!token || !token.user.equals(req.user._id)) + throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST); + token.valid = false; + await LoginToken.save(token); + res.json({ success: true }); + } +); diff --git a/src/api/user/twofactor/backup/index.ts b/src/api/user/twofactor/backup/index.ts index 231f45b..e7cb6e6 100644 --- a/src/api/user/twofactor/backup/index.ts +++ b/src/api/user/twofactor/backup/index.ts @@ -1,7 +1,10 @@ -import { Router } from "express" +import { Router } from "express"; import Stacker from "../../../middlewares/stacker"; import { GetUserMiddleware } from "../../../middlewares/user"; -import TwoFactor, { TFATypes as TwoFATypes, IBackupCode } from "../../../../models/twofactor"; +import TwoFactor, { + TFATypes as TwoFATypes, + IBackupCode, +} from "../../../../models/twofactor"; import RequestError, { HttpStatusCode } from "../../../../helper/request_error"; import moment = require("moment"); import { upgradeToken } from "../helper"; @@ -15,60 +18,83 @@ function generateCode(length: number) { let bytes = crypto.randomBytes(length); let nrs = ""; bytes.forEach((b, idx) => { - let nr = Math.floor((b / 255) * 9.9999) + let nr = Math.floor((b / 255) * 9.9999); if (nr > 9) nr = 9; nrs += String(nr); - }) + }); return nrs; } -BackupCodeRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { - //Generating new - let codes = Array(10).map(() => generateCode(8)); - console.log(codes); - let twofactor = TwoFactor.new({ - user: req.user._id, - type: TwoFATypes.OTC, - valid: true, - data: codes, - name: "" - }) - await TwoFactor.save(twofactor); - res.json({ - codes, - id: twofactor._id - }) -})); - -BackupCodeRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { - let { login, special } = req.token; - let { id, code }: { id: string, code: string } = req.body; - - let twofactor: IBackupCode = await TwoFactor.findById(id); - - if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) { - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } - - if (twofactor.expires && moment().isAfter(twofactor.expires)) { - twofactor.valid = false; +BackupCodeRoute.post( + "/", + Stacker(GetUserMiddleware(true, true), async (req, res) => { + //Generating new + let codes = Array(10).map(() => generateCode(8)); + console.log(codes); + let twofactor = TwoFactor.new({ + user: req.user._id, + type: TwoFATypes.OTC, + valid: true, + data: codes, + name: "", + }); await TwoFactor.save(twofactor); - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } - code = code.replace(/\s/g, ""); - let valid = twofactor.data.find(c => c === code); + res.json({ + codes, + id: twofactor._id, + }); + }) +); - if (valid) { - twofactor.data = twofactor.data.filter(c => c !== code); - await TwoFactor.save(twofactor); - let [login_exp, special_exp] = await Promise.all([ - upgradeToken(login), - upgradeToken(special) - ]); - res.json({ success: true, login_exp, special_exp }) - } else { - throw new RequestError("Invalid or already used code!", HttpStatusCode.BAD_REQUEST); - } -})) +BackupCodeRoute.put( + "/", + Stacker( + GetUserMiddleware(true, false, undefined, false), + async (req, res) => { + let { login, special } = req.token; + let { id, code }: { id: string; code: string } = req.body; + + let twofactor: IBackupCode = await TwoFactor.findById(id); + + if ( + !twofactor || + !twofactor.valid || + !twofactor.user.equals(req.user._id) || + twofactor.type !== TwoFATypes.OTC + ) { + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + + if (twofactor.expires && moment().isAfter(twofactor.expires)) { + twofactor.valid = false; + await TwoFactor.save(twofactor); + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + code = code.replace(/\s/g, ""); + let valid = twofactor.data.find((c) => c === code); + + if (valid) { + twofactor.data = twofactor.data.filter((c) => c !== code); + await TwoFactor.save(twofactor); + let [login_exp, special_exp] = await Promise.all([ + upgradeToken(login), + upgradeToken(special), + ]); + res.json({ success: true, login_exp, special_exp }); + } else { + throw new RequestError( + "Invalid or already used code!", + HttpStatusCode.BAD_REQUEST + ); + } + } + ) +); export default BackupCodeRoute; diff --git a/src/api/user/twofactor/helper.ts b/src/api/user/twofactor/helper.ts index d2f8896..51e8ed9 100644 --- a/src/api/user/twofactor/helper.ts +++ b/src/api/user/twofactor/helper.ts @@ -6,8 +6,11 @@ export async function upgradeToken(token: ILoginToken) { token.valid = true; token.validated = true; //TODO durations from config - let expires = (token.special ? moment().add(30, "minute") : moment().add(6, "months")).toDate(); + let expires = (token.special + ? moment().add(30, "minute") + : moment().add(6, "months") + ).toDate(); token.validTill = expires; await LoginToken.save(token); return expires; -} \ No newline at end of file +} diff --git a/src/api/user/twofactor/index.ts b/src/api/user/twofactor/index.ts index e3d4a2c..3ceabcf 100644 --- a/src/api/user/twofactor/index.ts +++ b/src/api/user/twofactor/index.ts @@ -3,44 +3,54 @@ import YubiKeyRoute from "./yubikey"; import { GetUserMiddleware } from "../../middlewares/user"; import Stacker from "../../middlewares/stacker"; import TwoFactor from "../../../models/twofactor"; -import * as moment from "moment" +import * as moment from "moment"; import RequestError, { HttpStatusCode } from "../../../helper/request_error"; import OTCRoute from "./otc"; import BackupCodeRoute from "./backup"; const TwoFactorRouter = Router(); -TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { - let twofactor = await TwoFactor.find({ user: req.user._id, valid: true }) - let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false) - await Promise.all(expired.map(e => { - e.valid = false; - return TwoFactor.save(e); - })); - twofactor = twofactor.filter(e => e.valid); - let tfa = twofactor.map(e => { - return { - id: e._id, - name: e.name, - type: e.type - } +TwoFactorRouter.get( + "/", + Stacker(GetUserMiddleware(true, true), async (req, res) => { + let twofactor = await TwoFactor.find({ user: req.user._id, valid: true }); + let expired = twofactor.filter((e) => + e.expires ? moment().isAfter(moment(e.expires)) : false + ); + await Promise.all( + expired.map((e) => { + e.valid = false; + return TwoFactor.save(e); + }) + ); + twofactor = twofactor.filter((e) => e.valid); + let tfa = twofactor.map((e) => { + return { + id: e._id, + name: e.name, + type: e.type, + }; + }); + res.json({ methods: tfa }); }) - res.json({ methods: tfa }); -})); +); -TwoFactorRouter.delete("/:id", Stacker(GetUserMiddleware(true, true), async (req, res) => { - let { id } = req.params; - let tfa = await TwoFactor.findById(id); - if (!tfa || !tfa.user.equals(req.user._id)) { - throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST); - } - tfa.valid = false; - await TwoFactor.save(tfa); - res.json({ success: true }); -})); +TwoFactorRouter.delete( + "/:id", + Stacker(GetUserMiddleware(true, true), async (req, res) => { + let { id } = req.params; + let tfa = await TwoFactor.findById(id); + if (!tfa || !tfa.user.equals(req.user._id)) { + throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST); + } + tfa.valid = false; + await TwoFactor.save(tfa); + res.json({ success: true }); + }) +); TwoFactorRouter.use("/yubikey", YubiKeyRoute); TwoFactorRouter.use("/otc", OTCRoute); TwoFactorRouter.use("/backup", BackupCodeRoute); -export default TwoFactorRouter; \ No newline at end of file +export default TwoFactorRouter; diff --git a/src/api/user/twofactor/otc/index.ts b/src/api/user/twofactor/otc/index.ts index ad85ac7..1a26cd2 100644 --- a/src/api/user/twofactor/otc/index.ts +++ b/src/api/user/twofactor/otc/index.ts @@ -1,7 +1,10 @@ -import { Router } from "express" +import { Router } from "express"; import Stacker from "../../../middlewares/stacker"; import { GetUserMiddleware } from "../../../middlewares/user"; -import TwoFactor, { TFATypes as TwoFATypes, IOTC } from "../../../../models/twofactor"; +import TwoFactor, { + TFATypes as TwoFATypes, + IOTC, +} from "../../../../models/twofactor"; import RequestError, { HttpStatusCode } from "../../../../helper/request_error"; import moment = require("moment"); import { upgradeToken } from "../helper"; @@ -13,93 +16,120 @@ import config from "../../../../config"; const OTCRoute = Router(); -OTCRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { - const { type } = req.query; - if (type === "create") { - //Generating new - let secret = speakeasy.generateSecret({ - name: config.core.name, - issuer: config.core.name - }); - let twofactor = TwoFactor.new({ - user: req.user._id, - type: TwoFATypes.OTC, - valid: false, - data: secret.base32 - }) - let dataurl = await qrcode.toDataURL(secret.otpauth_url); - await TwoFactor.save(twofactor); - res.json({ - image: dataurl, - id: twofactor._id - }) - } else if (type === "validate") { - // Checking code and marking as valid - const { code, id } = req.body; - Logging.debug(req.body, id); - let twofactor: IOTC = await TwoFactor.findById(id); - const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) }; - if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC || !twofactor.data || twofactor.valid) { - Logging.debug("Not found or wrong user", twofactor); - err(); - } - - if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) { - await TwoFactor.delete(twofactor); - Logging.debug("Expired!", twofactor); - err(); - } - - let valid = speakeasy.totp.verify({ - secret: twofactor.data, - encoding: "base32", - token: code - }) - - if (valid) { - twofactor.expires = undefined; - twofactor.valid = true; +OTCRoute.post( + "/", + Stacker(GetUserMiddleware(true, true), async (req, res) => { + const { type } = req.query; + if (type === "create") { + //Generating new + let secret = speakeasy.generateSecret({ + name: config.core.name, + issuer: config.core.name, + }); + let twofactor = TwoFactor.new({ + user: req.user._id, + type: TwoFATypes.OTC, + valid: false, + data: secret.base32, + }); + let dataurl = await qrcode.toDataURL(secret.otpauth_url); await TwoFactor.save(twofactor); - res.json({ success: true }); + res.json({ + image: dataurl, + id: twofactor._id, + }); + } else if (type === "validate") { + // Checking code and marking as valid + const { code, id } = req.body; + Logging.debug(req.body, id); + let twofactor: IOTC = await TwoFactor.findById(id); + const err = () => { + throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST); + }; + if ( + !twofactor || + !twofactor.user.equals(req.user._id) || + twofactor.type !== TwoFATypes.OTC || + !twofactor.data || + twofactor.valid + ) { + Logging.debug("Not found or wrong user", twofactor); + err(); + } + + if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) { + await TwoFactor.delete(twofactor); + Logging.debug("Expired!", twofactor); + err(); + } + + let valid = speakeasy.totp.verify({ + secret: twofactor.data, + encoding: "base32", + token: code, + }); + + if (valid) { + twofactor.expires = undefined; + twofactor.valid = true; + await TwoFactor.save(twofactor); + res.json({ success: true }); + } else { + throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST); + } } else { - throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST); + throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST); } - } else { - throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST); - } -})); - -OTCRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { - let { login, special } = req.token; - let { id, code } = req.body; - let twofactor: IOTC = await TwoFactor.findById(id); - - if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) { - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } - - if (twofactor.expires && moment().isAfter(twofactor.expires)) { - twofactor.valid = false; - await TwoFactor.save(twofactor); - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } - - - let valid = speakeasy.totp.verify({ - secret: twofactor.data, - encoding: "base32", - token: code }) +); - if (valid) { - let [login_exp, special_exp] = await Promise.all([ - upgradeToken(login), - upgradeToken(special) - ]); - res.json({ success: true, login_exp, special_exp }) - } else { - throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST); - } -})) +OTCRoute.put( + "/", + Stacker( + GetUserMiddleware(true, false, undefined, false), + async (req, res) => { + let { login, special } = req.token; + let { id, code } = req.body; + let twofactor: IOTC = await TwoFactor.findById(id); + + if ( + !twofactor || + !twofactor.valid || + !twofactor.user.equals(req.user._id) || + twofactor.type !== TwoFATypes.OTC + ) { + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + + if (twofactor.expires && moment().isAfter(twofactor.expires)) { + twofactor.valid = false; + await TwoFactor.save(twofactor); + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + + let valid = speakeasy.totp.verify({ + secret: twofactor.data, + encoding: "base32", + token: code, + }); + + if (valid) { + let [login_exp, special_exp] = await Promise.all([ + upgradeToken(login), + upgradeToken(special), + ]); + res.json({ success: true, login_exp, special_exp }); + } else { + throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST); + } + } + ) +); export default OTCRoute; diff --git a/src/api/user/twofactor/yubikey/index.ts b/src/api/user/twofactor/yubikey/index.ts index 981f3db..3808f66 100644 --- a/src/api/user/twofactor/yubikey/index.ts +++ b/src/api/user/twofactor/yubikey/index.ts @@ -1,9 +1,12 @@ -import { Router, Request } from "express" +import { Router, Request } from "express"; import Stacker from "../../../middlewares/stacker"; import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user"; import * as u2f from "u2f"; import config from "../../../../config"; -import TwoFactor, { TFATypes as TwoFATypes, IYubiKey } from "../../../../models/twofactor"; +import TwoFactor, { + TFATypes as TwoFATypes, + IYubiKey, +} from "../../../../models/twofactor"; import RequestError, { HttpStatusCode } from "../../../../helper/request_error"; import moment = require("moment"); import LoginToken from "../../../../models/login_token"; @@ -15,120 +18,189 @@ const U2FRoute = Router(); /** * Registerinf a new YubiKey */ -U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { - const { type } = req.query; - if (type === "challenge") { - const registrationRequest = u2f.request(config.core.url); +U2FRoute.post( + "/", + Stacker(GetUserMiddleware(true, true), async (req, res) => { + const { type } = req.query; + if (type === "challenge") { + const registrationRequest = u2f.request(config.core.url); - let twofactor = TwoFactor.new({ - user: req.user._id, - type: TwoFATypes.U2F, - valid: false, - data: { - registration: registrationRequest - } - }) - await TwoFactor.save(twofactor); - res.json({ request: registrationRequest, id: twofactor._id, appid: config.core.url }); - } else { - const { response, id } = req.body; - Logging.debug(req.body, id); - let twofactor: IYubiKey = await TwoFactor.findById(id); - const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) }; - if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.U2F || !twofactor.data.registration || twofactor.valid) { - Logging.debug("Not found or wrong user", twofactor); - err(); - } - - if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) { - await TwoFactor.delete(twofactor); - Logging.debug("Expired!", twofactor); - err(); - } - - const result = u2f.checkRegistration(twofactor.data.registration, response); - - if (result.successful) { - twofactor.data = { - keyHandle: result.keyHandle, - publicKey: result.publicKey - } - twofactor.expires = undefined; - twofactor.valid = true; + let twofactor = TwoFactor.new({ + user: req.user._id, + type: TwoFATypes.U2F, + valid: false, + data: { + registration: registrationRequest, + }, + }); await TwoFactor.save(twofactor); - res.json({ success: true }); + res.json({ + request: registrationRequest, + id: twofactor._id, + appid: config.core.url, + }); } else { - throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); - } - } -})); + const { response, id } = req.body; + Logging.debug(req.body, id); + let twofactor: IYubiKey = await TwoFactor.findById(id); + const err = () => { + throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST); + }; + if ( + !twofactor || + !twofactor.user.equals(req.user._id) || + twofactor.type !== TwoFATypes.U2F || + !twofactor.data.registration || + twofactor.valid + ) { + Logging.debug("Not found or wrong user", twofactor); + err(); + } -U2FRoute.get("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { - let { login, special } = req.token; - let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true }) + if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) { + await TwoFactor.delete(twofactor); + Logging.debug("Expired!", twofactor); + err(); + } - if (!twofactor) { - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } + const result = u2f.checkRegistration( + twofactor.data.registration, + response + ); - if (twofactor.expires) { - if (moment().isAfter(twofactor.expires)) { - twofactor.valid = false; - await TwoFactor.save(twofactor); - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } - } - - let request = u2f.request(config.core.url, twofactor.data.keyHandle); - login.data = { - type: "ykr", - request - }; - let r;; - if (special) { - special.data = login.data; - r = LoginToken.save(special); - } - - await Promise.all([r, LoginToken.save(login)]); - res.json({ request }); -})) - -U2FRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { - let { login, special } = req.token; - let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true }) - - - let { response } = req.body; - if (!twofactor || !login.data || login.data.type !== "ykr" || special && (!special.data || special.data.type !== "ykr")) { - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } - - if (twofactor.expires && moment().isAfter(twofactor.expires)) { - twofactor.valid = false; - await TwoFactor.save(twofactor); - throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); - } - - let login_exp; - let special_exp; - let result = u2f.checkSignature(login.data.request, response, twofactor.data.publicKey); - if (result.successful) { - if (special) { - let result = u2f.checkSignature(special.data.request, response, twofactor.data.publicKey); if (result.successful) { - special_exp = await upgradeToken(special); - } - else { - throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); + twofactor.data = { + keyHandle: result.keyHandle, + publicKey: result.publicKey, + }; + twofactor.expires = undefined; + twofactor.valid = true; + await TwoFactor.save(twofactor); + res.json({ success: true }); + } else { + throw new RequestError( + result.errorMessage, + HttpStatusCode.BAD_REQUEST + ); } } - login_exp = await upgradeToken(login); - } - else { - throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); - } - res.json({ success: true, login_exp, special_exp }) -})) + }) +); + +U2FRoute.get( + "/", + Stacker( + GetUserMiddleware(true, false, undefined, false), + async (req, res) => { + let { login, special } = req.token; + let twofactor: IYubiKey = await TwoFactor.findOne({ + user: req.user._id, + type: TwoFATypes.U2F, + valid: true, + }); + + if (!twofactor) { + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + + if (twofactor.expires) { + if (moment().isAfter(twofactor.expires)) { + twofactor.valid = false; + await TwoFactor.save(twofactor); + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + } + + let request = u2f.request(config.core.url, twofactor.data.keyHandle); + login.data = { + type: "ykr", + request, + }; + let r; + if (special) { + special.data = login.data; + r = LoginToken.save(special); + } + + await Promise.all([r, LoginToken.save(login)]); + res.json({ request }); + } + ) +); + +U2FRoute.put( + "/", + Stacker( + GetUserMiddleware(true, false, undefined, false), + async (req, res) => { + let { login, special } = req.token; + let twofactor: IYubiKey = await TwoFactor.findOne({ + user: req.user._id, + type: TwoFATypes.U2F, + valid: true, + }); + + let { response } = req.body; + if ( + !twofactor || + !login.data || + login.data.type !== "ykr" || + (special && (!special.data || special.data.type !== "ykr")) + ) { + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + + if (twofactor.expires && moment().isAfter(twofactor.expires)) { + twofactor.valid = false; + await TwoFactor.save(twofactor); + throw new RequestError( + "Invalid Method!", + HttpStatusCode.BAD_REQUEST + ); + } + + let login_exp; + let special_exp; + let result = u2f.checkSignature( + login.data.request, + response, + twofactor.data.publicKey + ); + if (result.successful) { + if (special) { + let result = u2f.checkSignature( + special.data.request, + response, + twofactor.data.publicKey + ); + if (result.successful) { + special_exp = await upgradeToken(special); + } else { + throw new RequestError( + result.errorMessage, + HttpStatusCode.BAD_REQUEST + ); + } + } + login_exp = await upgradeToken(login); + } else { + throw new RequestError( + result.errorMessage, + HttpStatusCode.BAD_REQUEST + ); + } + res.json({ success: true, login_exp, special_exp }); + } + ) +); export default U2FRoute; diff --git a/src/config.ts b/src/config.ts index 6d79247..fa5f1f0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,64 +7,68 @@ import * as ini from "ini"; dotenv.config(); export interface DatabaseConfig { - host: string - database: string + host: string; + database: string; } export interface WebConfig { - port: string - secure: "true" | "false" | undefined + port: string; + secure: "true" | "false" | undefined; } export interface CoreConfig { - name: string - url: string - dev: boolean + name: string; + url: string; + dev: boolean; } export interface Config { - core: CoreConfig - database: DatabaseConfig - web: WebConfig + core: CoreConfig; + database: DatabaseConfig; + web: WebConfig; } -const config = parse({ - core: { - dev: { - default: false, - type: Boolean +const config = (parse( + { + core: { + dev: { + default: false, + type: Boolean, + }, + name: { + type: String, + default: "Open Auth", + }, + url: String, }, - name: { - type: String, - default: "Open Auth" - }, - url: String - }, - database: { database: { - type: String, - default: "openauth" + database: { + type: String, + default: "openauth", + }, + host: { + type: String, + default: "localhost", + }, + }, + web: { + port: { + type: Number, + default: 3004, + }, + secure: { + type: Boolean, + default: false, + }, }, - host: { - type: String, - default: "localhost" - } }, - web: { - port: { - type: Number, - default: 3004 - }, - secure: { - type: Boolean, - default: false - } - } -}, "config.ini") as any as Config; + "config.ini" +) as any) as Config; -if (process.env.DEV === "true") - config.core.dev = true; +if (process.env.DEV === "true") config.core.dev = true; if (config.core.dev) - Logging.warning("DEV mode active. This can cause major performance issues, data loss and vulnerabilities! ") + Logging.warning( + "DEV mode active. This can cause major performance issues, data loss and vulnerabilities! " + ); -export default config; \ No newline at end of file +export default config; diff --git a/src/database.ts b/src/database.ts index b65a9eb..e38474f 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,11 +1,11 @@ import SafeMongo from "@hibas123/safe_mongo"; -import Config from "./config" -let dbname = "openauth" -let host = "localhost" +import Config from "./config"; +let dbname = "openauth"; +let host = "localhost"; if (Config.database) { if (Config.database.database) dbname = Config.database.database; if (Config.database.host) host = Config.database.host; } if (Config.core.dev) dbname += "_dev"; const DB = new SafeMongo("mongodb://" + host, dbname); -export default DB; \ No newline at end of file +export default DB; diff --git a/src/express.d.ts b/src/express.d.ts index 808fecd..b004258 100644 --- a/src/express.d.ts +++ b/src/express.d.ts @@ -11,6 +11,6 @@ declare module "express" { token: { login: ILoginToken; special?: ILoginToken; - } + }; } -} \ No newline at end of file +} diff --git a/src/helper/jwt.ts b/src/helper/jwt.ts index 67e16d1..7d141aa 100644 --- a/src/helper/jwt.ts +++ b/src/helper/jwt.ts @@ -9,42 +9,52 @@ export interface OAuthJWT { user: string; username: string; permissions: string[]; - application: string + application: string; } const issuer = config.core.url; export const IDTokenJWTExp = moment.duration(30, "m").asSeconds(); export function getIDToken(user: IUser, client_id: string, nonce: string) { - return createJWT({ - user: user.uid, - name: user.name, - nickname: user.username, - username: user.username, - preferred_username: user.username, - gender: Gender[user.gender], - nonce - }, { - expiresIn: IDTokenJWTExp, - issuer, - algorithm: "RS256", - subject: user.uid, - audience: client_id - }) + return createJWT( + { + user: user.uid, + name: user.name, + nickname: user.username, + username: user.username, + preferred_username: user.username, + gender: Gender[user.gender], + nonce, + }, + { + expiresIn: IDTokenJWTExp, + issuer, + algorithm: "RS256", + subject: user.uid, + audience: client_id, + } + ); } export const AccessTokenJWTExp = moment.duration(6, "h"); -export function getAccessTokenJWT(token: { user: IUser, permissions: ObjectID[], client: IClient }) { - return createJWT({ - user: token.user.uid, - username: token.user.username, - permissions: token.permissions.map(p => p.toHexString()), - application: token.client.client_id - }, { - expiresIn: AccessTokenJWTExp.asSeconds(), - issuer, - algorithm: "RS256", - subject: token.user.uid, - audience: token.client.client_id - }) -} \ No newline at end of file +export function getAccessTokenJWT(token: { + user: IUser; + permissions: ObjectID[]; + client: IClient; +}) { + return createJWT( + { + user: token.user.uid, + username: token.user.username, + permissions: token.permissions.map((p) => p.toHexString()), + application: token.client.client_id, + }, + { + expiresIn: AccessTokenJWTExp.asSeconds(), + issuer, + algorithm: "RS256", + subject: token.user.uid, + audience: token.client.client_id, + } + ); +} diff --git a/src/helper/promiseMiddleware.ts b/src/helper/promiseMiddleware.ts index c769171..aa144d3 100644 --- a/src/helper/promiseMiddleware.ts +++ b/src/helper/promiseMiddleware.ts @@ -1,5 +1,7 @@ -import { Request, Response, NextFunction } from "express" +import { Request, Response, NextFunction } from "express"; -export default (fn: (req: Request, res: Response, next: NextFunction) => Promise) => (req: Request, res: Response, next: NextFunction) => { - Promise.resolve(fn(req, res, next)).catch(next) -} \ No newline at end of file +export default ( + fn: (req: Request, res: Response, next: NextFunction) => Promise +) => (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; diff --git a/src/helper/random.ts b/src/helper/random.ts index 9100803..05aaa43 100644 --- a/src/helper/random.ts +++ b/src/helper/random.ts @@ -2,4 +2,4 @@ import { randomBytes } from "crypto"; export function randomString(length: number) { return randomBytes(length).toString("base64").slice(0, length); -} \ No newline at end of file +} diff --git a/src/helper/request_error.ts b/src/helper/request_error.ts index 8bf38c9..c5f5a83 100644 --- a/src/helper/request_error.ts +++ b/src/helper/request_error.ts @@ -1,10 +1,8 @@ - /** * Hypertext Transfer Protocol (HTTP) response status codes. * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} */ export enum HttpStatusCode { - /** * The server has received the request headers and the client should proceed to send the request body * (in the case of a request for which a body needs to be sent; for example, a POST request). @@ -376,13 +374,17 @@ export enum HttpStatusCode { * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). */ - NETWORK_AUTHENTICATION_REQUIRED = 511 + NETWORK_AUTHENTICATION_REQUIRED = 511, } - export default class RequestError extends Error { - constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false, public additional: any = undefined) { - super("") + constructor( + message: any, + public status: HttpStatusCode, + public nolog: boolean = false, + public additional: any = undefined + ) { + super(""); this.message = message; } -} \ No newline at end of file +} diff --git a/src/helper/user_key.ts b/src/helper/user_key.ts index 864dd4b..2aa3559 100644 --- a/src/helper/user_key.ts +++ b/src/helper/user_key.ts @@ -1,14 +1,18 @@ // import * as crypto from "crypto-js" import { IUser } from "../models/user"; import { IClient } from "../models/client"; -import * as crypto from "crypto" +import * as crypto from "crypto"; function sha512(text: string) { - let hash = crypto.createHash("sha512") - hash.update(text) - return hash.digest("base64") + let hash = crypto.createHash("sha512"); + hash.update(text); + return hash.digest("base64"); } export function getEncryptionKey(user: IUser, client: IClient) { - return sha512(sha512(user.encryption_key) + sha512(client._id.toHexString()) + sha512(client.client_id)) -} \ No newline at end of file + return sha512( + sha512(user.encryption_key) + + sha512(client._id.toHexString()) + + sha512(client.client_id) + ); +} diff --git a/src/index.ts b/src/index.ts index 6f69a2f..c30149a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,67 +13,78 @@ import config from "./config"; // } if (!config.web) { - Logging.error("No web config set. Terminating.") + Logging.error("No web config set. Terminating."); process.exit(); } -import * as i18n from "i18n" +import * as i18n from "i18n"; i18n.configure({ locales: ["en", "de"], directory: "./locales", -}) +}); import Web from "./web"; import TestData from "./testdata"; import DB from "./database"; -Logging.log("Connecting to Database") +Logging.log("Connecting to Database"); if (config.core.dev) { - Logging.warning("Running in dev mode! Database will be cleared!") + Logging.warning("Running in dev mode! Database will be cleared!"); } -DB.connect().then(async () => { - Logging.log("Database connected") - if (config.core.dev) - await TestData() - let web = new Web(config.web) - web.listen() +DB.connect() + .then(async () => { + Logging.log("Database connected"); + if (config.core.dev) await TestData(); + let web = new Web(config.web); + web.listen(); - let already = new Set(); - function print(path, layer) { - if (layer.route) { - layer.route.stack.forEach(print.bind(null, path.concat(split(layer.route.path)))) - } else if (layer.name === 'router' && layer.handle.stack) { - layer.handle.stack.forEach(print.bind(null, path.concat(split(layer.regexp)))) - } else if (layer.method) { - let me: string = layer.method.toUpperCase(); - me += " ".repeat(6 - me.length); - let msg = `${me} /${path.concat(split(layer.regexp)).filter(Boolean).join('/')}`; - if (!already.has(msg)) { - already.add(msg); - Logging.log(msg); + let already = new Set(); + function print(path, layer) { + if (layer.route) { + layer.route.stack.forEach( + print.bind(null, path.concat(split(layer.route.path))) + ); + } else if (layer.name === "router" && layer.handle.stack) { + layer.handle.stack.forEach( + print.bind(null, path.concat(split(layer.regexp))) + ); + } else if (layer.method) { + let me: string = layer.method.toUpperCase(); + me += " ".repeat(6 - me.length); + let msg = `${me} /${path + .concat(split(layer.regexp)) + .filter(Boolean) + .join("/")}`; + if (!already.has(msg)) { + already.add(msg); + Logging.log(msg); + } } } - } - function split(thing) { - if (typeof thing === 'string') { - return thing.split('/') - } else if (thing.fast_slash) { - return '' - } else { - var match = thing.toString() - .replace('\\/?', '') - .replace('(?=\\/|$)', '$') - .match(/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//) - return match - ? match[1].replace(/\\(.)/g, '$1').split('/') - : '' + function split(thing) { + if (typeof thing === "string") { + return thing.split("/"); + } else if (thing.fast_slash) { + return ""; + } else { + var match = thing + .toString() + .replace("\\/?", "") + .replace("(?=\\/|$)", "$") + .match( + /^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\// + ); + return match + ? match[1].replace(/\\(.)/g, "$1").split("/") + : ""; + } } - } - // Logging.log("--- Endpoints: ---"); - // web.server._router.stack.forEach(print.bind(null, [])) - // Logging.log("--- Endpoints end ---") -}).catch(e => { - Logging.error(e) - process.exit(); -}) \ No newline at end of file + // Logging.log("--- Endpoints: ---"); + // web.server._router.stack.forEach(print.bind(null, [])) + // Logging.log("--- Endpoints end ---") + }) + .catch((e) => { + Logging.error(e); + process.exit(); + }); diff --git a/src/keys.ts b/src/keys.ts index aa65b58..2d607b4 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -1,10 +1,10 @@ import Logging from "@hibas123/nodelogging"; -import * as fs from "fs" +import * as fs from "fs"; let private_key: string; let rsa: RSA; export function sign(message: Buffer): Buffer { - return rsa.sign(message, "buffer") + return rsa.sign(message, "buffer"); } export function verify(message: Buffer, signature: Buffer): boolean { @@ -19,28 +19,28 @@ import config from "./config"; export function createJWT(payload: any, options: jwt.SignOptions) { return new Promise((resolve, reject) => { return jwt.sign(payload, private_key, options, (err, token) => { - if (err) reject(err) - else resolve(token) + if (err) reject(err); + else resolve(token); }); - }) + }); } export async function validateJWT(data: string) { return new Promise((resolve, reject) => { jwt.verify(data, public_key, (err, valid) => { - if (err) reject(err) - else resolve(valid) + if (err) reject(err); + else resolve(valid); }); - }) + }); } let create = false; if (fs.existsSync("./keys")) { if (fs.existsSync("./keys/private.pem")) { if (fs.existsSync("./keys/public.pem")) { - Logging.log("Using existing private and public key") - private_key = fs.readFileSync("./keys/private.pem").toString("utf8") - public_key = fs.readFileSync("./keys/public.pem").toString("utf8") + Logging.log("Using existing private and public key"); + private_key = fs.readFileSync("./keys/private.pem").toString("utf8"); + public_key = fs.readFileSync("./keys/public.pem").toString("utf8"); if (!private_key || !public_key) { create = true; @@ -49,21 +49,21 @@ if (fs.existsSync("./keys")) { } else create = true; } else create = true; -import * as RSA from "node-rsa" +import * as RSA from "node-rsa"; if (create === true) { - Logging.log("Started RSA Key gen") + Logging.log("Started RSA Key gen"); let rsa = new RSA({ b: 4096 }); - private_key = rsa.exportKey("private") - public_key = rsa.exportKey("public") + private_key = rsa.exportKey("private"); + public_key = rsa.exportKey("public"); if (!fs.existsSync("./keys")) { - fs.mkdirSync("./keys") + fs.mkdirSync("./keys"); } - fs.writeFileSync("./keys/private.pem", private_key) - fs.writeFileSync("./keys/public.pem", public_key) - Logging.log("Key pair generated") + fs.writeFileSync("./keys/private.pem", private_key); + fs.writeFileSync("./keys/public.pem", public_key); + Logging.log("Key pair generated"); } -rsa = new RSA(private_key, "private") -rsa.importKey(public_key, "public") +rsa = new RSA(private_key, "private"); +rsa.importKey(public_key, "public"); diff --git a/src/models/client.ts b/src/models/client.ts index 2e2a576..e5e01a0 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -4,21 +4,21 @@ import { ObjectID } from "mongodb"; import { v4 } from "uuid"; export interface IClient extends ModelDataBase { - maintainer: ObjectID - internal: boolean - name: string - redirect_url: string - website: string - logo?: string - client_id: string - client_secret: string + maintainer: ObjectID; + internal: boolean; + name: string; + redirect_url: string; + website: string; + logo?: string; + client_id: string; + client_secret: string; } const Client = DB.addModel({ name: "client", versions: [ { - migration: () => { }, + migration: () => {}, schema: { maintainer: { type: ObjectID }, internal: { type: Boolean, default: false }, @@ -27,10 +27,10 @@ const Client = DB.addModel({ website: { type: String }, logo: { type: String, optional: true }, client_id: { type: String, default: () => v4() }, - client_secret: { type: String } - } - } - ] -}) + client_secret: { type: String }, + }, + }, + ], +}); -export default Client; \ No newline at end of file +export default Client; diff --git a/src/models/client_code.ts b/src/models/client_code.ts index 7a20cad..b3208c5 100644 --- a/src/models/client_code.ts +++ b/src/models/client_code.ts @@ -4,24 +4,26 @@ import { ObjectID } from "mongodb"; import { v4 } from "uuid"; export interface IClientCode extends ModelDataBase { - user: ObjectID + user: ObjectID; code: string; - client: ObjectID - permissions: ObjectID[] + client: ObjectID; + permissions: ObjectID[]; validTill: Date; } const ClientCode = DB.addModel({ name: "client_code", - versions: [{ - migration: () => { }, - schema: { - user: { type: ObjectID }, - code: { type: String }, - client: { type: ObjectID }, - permissions: { type: Array }, - validTill: { type: Date } - } - }] + versions: [ + { + migration: () => {}, + schema: { + user: { type: ObjectID }, + code: { type: String }, + client: { type: ObjectID }, + permissions: { type: Array }, + validTill: { type: Date }, + }, + }, + ], }); -export default ClientCode; \ No newline at end of file +export default ClientCode; diff --git a/src/models/grants.ts b/src/models/grants.ts index 5112c37..0c18bae 100644 --- a/src/models/grants.ts +++ b/src/models/grants.ts @@ -10,14 +10,16 @@ export interface IGrant extends ModelDataBase { const Grant = DB.addModel({ name: "grant", - versions: [{ - migration: () => { }, - schema: { - user: { type: ObjectID }, - client: { type: ObjectID }, - permissions: { type: ObjectID, array: true } - } - }] -}) + versions: [ + { + migration: () => {}, + schema: { + user: { type: ObjectID }, + client: { type: ObjectID }, + permissions: { type: ObjectID, array: true }, + }, + }, + ], +}); export default Grant; diff --git a/src/models/login_token.ts b/src/models/login_token.ts index 36f37d2..47af61d 100644 --- a/src/models/login_token.ts +++ b/src/models/login_token.ts @@ -16,52 +16,61 @@ export interface ILoginToken extends ModelDataBase { } const LoginToken = DB.addModel({ name: "login_token", - versions: [{ - migration: () => { }, - schema: { - token: { type: String }, - special: { type: Boolean, default: () => false }, - user: { type: ObjectID }, - validTill: { type: Date }, - valid: { type: Boolean } - } - }, { - migration: (doc: ILoginToken) => { doc.validated = true; }, - schema: { - token: { type: String }, - special: { type: Boolean, default: () => false }, - user: { type: ObjectID }, - validTill: { type: Date }, - valid: { type: Boolean }, - validated: { type: Boolean, default: false } - } - }, { - migration: (doc: ILoginToken) => { doc.validated = true; }, - schema: { - token: { type: String }, - special: { type: Boolean, default: () => false }, - user: { type: ObjectID }, - validTill: { type: Date }, - valid: { type: Boolean }, - validated: { type: Boolean, default: false }, - data: { type: "any", optional: true }, - ip: { type: String, optional: true }, - browser: { type: String, optional: true } - } - }] -}) + versions: [ + { + migration: () => {}, + schema: { + token: { type: String }, + special: { type: Boolean, default: () => false }, + user: { type: ObjectID }, + validTill: { type: Date }, + valid: { type: Boolean }, + }, + }, + { + migration: (doc: ILoginToken) => { + doc.validated = true; + }, + schema: { + token: { type: String }, + special: { type: Boolean, default: () => false }, + user: { type: ObjectID }, + validTill: { type: Date }, + valid: { type: Boolean }, + validated: { type: Boolean, default: false }, + }, + }, + { + migration: (doc: ILoginToken) => { + doc.validated = true; + }, + schema: { + token: { type: String }, + special: { type: Boolean, default: () => false }, + user: { type: ObjectID }, + validTill: { type: Date }, + valid: { type: Boolean }, + validated: { type: Boolean, default: false }, + data: { type: "any", optional: true }, + ip: { type: String, optional: true }, + browser: { type: String, optional: true }, + }, + }, + ], +}); -export async function CheckToken(token: ILoginToken, validated: boolean = true): Promise { - if (!token || !token.valid) - return false; - if (validated && !token.validated) - return false; +export async function CheckToken( + token: ILoginToken, + validated: boolean = true +): Promise { + if (!token || !token.valid) return false; + if (validated && !token.validated) return false; if (moment().isAfter(token.validTill)) { token.valid = false; - await LoginToken.save(token) + await LoginToken.save(token); return false; } return true; } -export default LoginToken; \ No newline at end of file +export default LoginToken; diff --git a/src/models/mail.ts b/src/models/mail.ts index 04bc8e9..51b1289 100644 --- a/src/models/mail.ts +++ b/src/models/mail.ts @@ -9,14 +9,16 @@ export interface IMail extends ModelDataBase { const Mail = DB.addModel({ name: "mail", - versions: [{ - migration: () => { }, - schema: { - mail: { type: String }, - verified: { type: Boolean, default: false }, - primary: { type: Boolean } - } - }] -}) + versions: [ + { + migration: () => {}, + schema: { + mail: { type: String }, + verified: { type: Boolean, default: false }, + primary: { type: Boolean }, + }, + }, + ], +}); -export default Mail; \ No newline at end of file +export default Mail; diff --git a/src/models/permissions.ts b/src/models/permissions.ts index e408eec..04eaadc 100644 --- a/src/models/permissions.ts +++ b/src/models/permissions.ts @@ -11,22 +11,27 @@ export interface IPermission extends ModelDataBase { const Permission = DB.addModel({ name: "permission", - versions: [{ - migration: () => { }, - schema: { - name: { type: String }, - description: { type: String }, - client: { type: ObjectID } - } - }, { - migration: (old) => { old.grant_type = "user" }, - schema: { - name: { type: String }, - description: { type: String }, - client: { type: ObjectID }, - grant_type: { type: String, default: "user" } - } - }] -}) + versions: [ + { + migration: () => {}, + schema: { + name: { type: String }, + description: { type: String }, + client: { type: ObjectID }, + }, + }, + { + migration: (old) => { + old.grant_type = "user"; + }, + schema: { + name: { type: String }, + description: { type: String }, + client: { type: ObjectID }, + grant_type: { type: String, default: "user" }, + }, + }, + ], +}); export default Permission; diff --git a/src/models/refresh_token.ts b/src/models/refresh_token.ts index 69f61b7..d7c0b63 100644 --- a/src/models/refresh_token.ts +++ b/src/models/refresh_token.ts @@ -14,17 +14,19 @@ export interface IRefreshToken extends ModelDataBase { const RefreshToken = DB.addModel({ name: "refresh_token", - versions: [{ - migration: () => { }, - schema: { - token: { type: String }, - user: { type: ObjectID }, - client: { type: ObjectID }, - permissions: { type: Array }, - validTill: { type: Date }, - valid: { type: Boolean } - } - }] -}) + versions: [ + { + migration: () => {}, + schema: { + token: { type: String }, + user: { type: ObjectID }, + client: { type: ObjectID }, + permissions: { type: Array }, + validTill: { type: Date }, + valid: { type: Boolean }, + }, + }, + ], +}); -export default RefreshToken; \ No newline at end of file +export default RefreshToken; diff --git a/src/models/regcodes.ts b/src/models/regcodes.ts index d76bf00..39365aa 100644 --- a/src/models/regcodes.ts +++ b/src/models/regcodes.ts @@ -11,15 +11,17 @@ export interface IRegCode extends ModelDataBase { const RegCode = DB.addModel({ name: "reg_code", - versions: [{ - migration: () => { }, - schema: { - token: { type: String }, - valid: { type: Boolean }, - validTill: { type: Date } - } - }] -}) + versions: [ + { + migration: () => {}, + schema: { + token: { type: String }, + valid: { type: Boolean }, + validTill: { type: Date }, + }, + }, + ], +}); export default RegCode; @@ -52,4 +54,4 @@ export default RegCode; // @Column // @DeletedAt // deletionDate: Date; -// } \ No newline at end of file +// } diff --git a/src/models/twofactor.ts b/src/models/twofactor.ts index 76abbca..07c9bbc 100644 --- a/src/models/twofactor.ts +++ b/src/models/twofactor.ts @@ -6,7 +6,7 @@ export enum TFATypes { OTC, BACKUP_CODE, U2F, - APP_ALLOW + APP_ALLOW, } export const TFANames = new Map(); @@ -16,11 +16,11 @@ TFANames.set(TFATypes.U2F, "Security Key (U2F)"); TFANames.set(TFATypes.APP_ALLOW, "App Push"); export interface ITwoFactor extends ModelDataBase { - user: ObjectID - valid: boolean + user: ObjectID; + valid: boolean; expires?: Date; name?: string; - type: TFATypes + type: TFATypes; data: any; } @@ -33,7 +33,7 @@ export interface IYubiKey extends ITwoFactor { registration?: any; publicKey: string; keyHandle: string; - } + }; } export interface IU2F extends ITwoFactor { @@ -42,7 +42,7 @@ export interface IU2F extends ITwoFactor { publicKey: string; keyHandle: string; registration?: string; - } + }; } export interface IBackupCode extends ITwoFactor { @@ -51,17 +51,19 @@ export interface IBackupCode extends ITwoFactor { const TwoFactor = DB.addModel({ name: "twofactor", - versions: [{ - migration: (e) => { }, - schema: { - user: { type: ObjectID }, - valid: { type: Boolean }, - expires: { type: Date, optional: true }, - name: { type: String, optional: true }, - type: { type: Number }, - data: { type: "any" }, - } - }] + versions: [ + { + migration: (e) => {}, + schema: { + user: { type: ObjectID }, + valid: { type: Boolean }, + expires: { type: Date, optional: true }, + name: { type: String, optional: true }, + type: { type: Number }, + data: { type: "any" }, + }, + }, + ], }); -export default TwoFactor; \ No newline at end of file +export default TwoFactor; diff --git a/src/models/user.ts b/src/models/user.ts index 3d337e1..2342e2b 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -8,7 +8,7 @@ export enum Gender { none, male, female, - other + other, } export interface IUser extends ModelDataBase { @@ -16,112 +16,119 @@ export interface IUser extends ModelDataBase { username: string; name: string; - birthday?: Date + birthday?: Date; gender: Gender; admin: boolean; password: string; salt: string; mails: ObjectID[]; - phones: { phone: string, verified: boolean, primary: boolean }[]; + phones: { phone: string; verified: boolean; primary: boolean }[]; encryption_key: string; } const User = DB.addModel({ name: "user", - versions: [{ - migration: () => { }, - schema: { - uid: { type: String, default: () => v4() }, - username: { type: String }, - name: { type: String }, - birthday: { type: Date, optional: true }, - gender: { type: Number }, - admin: { type: Boolean }, - password: { type: String }, - salt: { type: String }, - mails: { type: Array, default: () => [] }, - phones: { - array: true, - model: true, - type: { - phone: { type: String }, - verified: { type: Boolean }, - primary: { type: Boolean } - } + versions: [ + { + migration: () => {}, + schema: { + uid: { type: String, default: () => v4() }, + username: { type: String }, + name: { type: String }, + birthday: { type: Date, optional: true }, + gender: { type: Number }, + admin: { type: Boolean }, + password: { type: String }, + salt: { type: String }, + mails: { type: Array, default: () => [] }, + phones: { + array: true, + model: true, + type: { + phone: { type: String }, + verified: { type: Boolean }, + primary: { type: Boolean }, + }, + }, + twofactor: { + array: true, + model: true, + type: { + token: { type: String }, + valid: { type: Boolean }, + type: { type: Number }, + }, + }, }, - twofactor: { - array: true, - model: true, - type: { - token: { type: String }, - valid: { type: Boolean }, - type: { type: Number } - } - } - } - }, { - migration: (e: IUser) => { e.encryption_key = randomString(64) }, - schema: { - uid: { type: String, default: () => v4() }, - username: { type: String }, - name: { type: String }, - birthday: { type: Date, optional: true }, - gender: { type: Number }, - admin: { type: Boolean }, - password: { type: String }, - salt: { type: String }, - mails: { type: Array, default: () => [] }, - phones: { - array: true, - model: true, - type: { - phone: { type: String }, - verified: { type: Boolean }, - primary: { type: Boolean } - } + }, + { + migration: (e: IUser) => { + e.encryption_key = randomString(64); }, - twofactor: { - array: true, - model: true, - type: { - token: { type: String }, - valid: { type: Boolean }, - type: { type: Number } - } + schema: { + uid: { type: String, default: () => v4() }, + username: { type: String }, + name: { type: String }, + birthday: { type: Date, optional: true }, + gender: { type: Number }, + admin: { type: Boolean }, + password: { type: String }, + salt: { type: String }, + mails: { type: Array, default: () => [] }, + phones: { + array: true, + model: true, + type: { + phone: { type: String }, + verified: { type: Boolean }, + primary: { type: Boolean }, + }, + }, + twofactor: { + array: true, + model: true, + type: { + token: { type: String }, + valid: { type: Boolean }, + type: { type: Number }, + }, + }, + encryption_key: { + type: String, + default: () => randomString(64), + }, }, - encryption_key: { - type: String, - default: () => randomString(64) - } - } - }, - { - migration: (e: any) => { delete e.twofactor }, - schema: { - uid: { type: String, default: () => v4() }, - username: { type: String }, - name: { type: String }, - birthday: { type: Date, optional: true }, - gender: { type: Number }, - admin: { type: Boolean }, - password: { type: String }, - salt: { type: String }, - mails: { type: Array, default: () => [] }, - phones: { - array: true, - model: true, - type: { - phone: { type: String }, - verified: { type: Boolean }, - primary: { type: Boolean } - } + }, + { + migration: (e: any) => { + delete e.twofactor; }, - encryption_key: { - type: String, - default: () => randomString(64) - } - } - }] -}) + schema: { + uid: { type: String, default: () => v4() }, + username: { type: String }, + name: { type: String }, + birthday: { type: Date, optional: true }, + gender: { type: Number }, + admin: { type: Boolean }, + password: { type: String }, + salt: { type: String }, + mails: { type: Array, default: () => [] }, + phones: { + array: true, + model: true, + type: { + phone: { type: String }, + verified: { type: Boolean }, + primary: { type: Boolean }, + }, + }, + encryption_key: { + type: String, + default: () => randomString(64), + }, + }, + }, + ], +}); -export default User; \ No newline at end of file +export default User; diff --git a/src/testdata.ts b/src/testdata.ts index bd16877..c001f19 100644 --- a/src/testdata.ts +++ b/src/testdata.ts @@ -8,7 +8,6 @@ import { ObjectID } from "bson"; import DB from "./database"; import TwoFactor from "./models/twofactor"; - import * as speakeasy from "speakeasy"; import LoginToken from "./models/login_token"; import Mail from "./models/mail"; @@ -21,13 +20,12 @@ export default async function TestData() { mail = Mail.new({ mail: "test@test.de", primary: true, - verified: true - }) + verified: true, + }); await Mail.save(mail); } - let u = await User.findOne({ username: "test" }); if (!u) { Logging.log("Adding test user"); @@ -36,21 +34,22 @@ export default async function TestData() { birthday: new Date(), gender: Gender.male, name: "Test Test", - password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc", + password: + "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc", salt: "test", admin: true, phones: [ { phone: "+4915962855955", primary: true, verified: true }, - { phone: "+4915962855932", primary: false, verified: false } + { phone: "+4915962855932", primary: false, verified: false }, ], - mails: [mail._id] - }) + mails: [mail._id], + }); await User.save(u); } let c = await Client.findOne({ client_id: "test001" }); if (!c) { - Logging.log("Adding test client") + Logging.log("Adding test client"); c = Client.new({ client_id: "test001", client_secret: "test001", @@ -58,19 +57,19 @@ export default async function TestData() { maintainer: u._id, name: "Test Client", website: "http://example.com", - redirect_url: "http://example.com" - }) + redirect_url: "http://example.com", + }); await Client.save(c); } let perm = await Permission.findOne({ id: 0 }); if (!perm) { - Logging.log("Adding test permission") + Logging.log("Adding test permission"); perm = Permission.new({ _id: new ObjectID("507f1f77bcf86cd799439011"), name: "TestPerm", description: "Permission just for testing purposes", - client: c._id + client: c._id, }); await (await (Permission as any)._collection).insertOne(perm); @@ -80,30 +79,29 @@ export default async function TestData() { let r = await RegCode.findOne({ token: "test" }); if (!r) { - Logging.log("Adding test reg_code") + Logging.log("Adding test reg_code"); r = RegCode.new({ token: "test", valid: true, - validTill: moment().add("1", "year").toDate() - }) + validTill: moment().add("1", "year").toDate(), + }); await RegCode.save(r); } - let t = await TwoFactor.findOne({ user: u._id, type: 0 }) + let t = await TwoFactor.findOne({ user: u._id, type: 0 }); if (!t) { t = TwoFactor.new({ user: u._id, type: 0, valid: true, data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ", - expires: null - }) + expires: null, + }); TwoFactor.save(t); } let login_token = await LoginToken.findOne({ token: "test01" }); - if (login_token) - await LoginToken.delete(login_token); + if (login_token) await LoginToken.delete(login_token); login_token = LoginToken.new({ browser: "DEMO", @@ -113,13 +111,12 @@ export default async function TestData() { valid: true, validTill: moment().add("10", "years").toDate(), user: u._id, - validated: true + validated: true, }); await LoginToken.save(login_token); let special_token = await LoginToken.findOne({ token: "test02" }); - if (special_token) - await LoginToken.delete(special_token); + if (special_token) await LoginToken.delete(special_token); special_token = LoginToken.new({ browser: "DEMO", @@ -129,11 +126,10 @@ export default async function TestData() { valid: true, validTill: moment().add("10", "years").toDate(), user: u._id, - validated: true + validated: true, }); await LoginToken.save(special_token); - // setInterval(() => { // let code = speakeasy.totp({ // secret: t.data, @@ -141,4 +137,4 @@ export default async function TestData() { // }) // Logging.debug("OTC Code is:", code); // }, 1000) -} +} diff --git a/src/views/admin.ts b/src/views/admin.ts index b82df87..841e64e 100644 --- a/src/views/admin.ts +++ b/src/views/admin.ts @@ -1,6 +1,6 @@ -import * as handlebars from "handlebars" +import * as handlebars from "handlebars"; import { readFileSync } from "fs"; -import { __ as i__ } from "i18n" +import { __ as i__ } from "i18n"; import config from "../config"; let template: handlebars.TemplateDelegate; @@ -11,11 +11,11 @@ function loadStatic() { export default function GetAdminPage(__: typeof i__): string { if (config.core.dev) { - loadStatic() + loadStatic(); } - let data = {} - return template(data, { helpers: { "i18n": __ } }) + let data = {}; + return template(data, { helpers: { i18n: __ } }); } -loadStatic() \ No newline at end of file +loadStatic(); diff --git a/src/views/authorize.ts b/src/views/authorize.ts index efd3e91..06ecc8c 100644 --- a/src/views/authorize.ts +++ b/src/views/authorize.ts @@ -1,26 +1,34 @@ -import { compile, TemplateDelegate } from "handlebars" +import { compile, TemplateDelegate } from "handlebars"; import { readFileSync } from "fs"; -import { __ as i__ } from "i18n" +import { __ as i__ } from "i18n"; import config from "../config"; let template: TemplateDelegate; function loadStatic() { let html = readFileSync("./views/out/authorize/authorize.html").toString(); - template = compile(html) + template = compile(html); } -export default function GetAuthPage(__: typeof i__, appname: string, scopes: { name: string, description: string, logo: string }[]): string { +export default function GetAuthPage( + __: typeof i__, + appname: string, + scopes: { name: string; description: string; logo: string }[] +): string { if (config.core.dev) { - loadStatic() + loadStatic(); } - return template({ - title: __("Authorize %s", appname), - information: __("By clicking on ALLOW, you allow this app to access the requested recources."), - scopes: scopes, - // request: request - }, { helpers: { "i18n": __ } }); + return template( + { + title: __("Authorize %s", appname), + information: __( + "By clicking on ALLOW, you allow this app to access the requested recources." + ), + scopes: scopes, + // request: request + }, + { helpers: { i18n: __ } } + ); } -loadStatic() - +loadStatic(); diff --git a/src/views/login.ts b/src/views/login.ts index 20d93ee..7fb027e 100644 --- a/src/views/login.ts +++ b/src/views/login.ts @@ -1,6 +1,6 @@ -import * as handlebars from "handlebars" +import * as handlebars from "handlebars"; import { readFileSync } from "fs"; -import { __ as i__ } from "i18n" +import { __ as i__ } from "i18n"; import config from "../config"; let template: handlebars.TemplateDelegate; @@ -16,8 +16,8 @@ function loadStatic() { * - prod 6sec * Mustache: * - dev : 15sec - * - prod: 12sec - * + * - prod: 12sec + * * Handlebars: * Compile + Render * - dev 13sec @@ -29,11 +29,11 @@ function loadStatic() { export default function GetLoginPage(__: typeof i__): string { if (config.core.dev) { - loadStatic() + loadStatic(); } - let data = {} - return template(data, { helpers: { "i18n": __ } }); + let data = {}; + return template(data, { helpers: { i18n: __ } }); } -loadStatic() \ No newline at end of file +loadStatic(); diff --git a/src/views/register.ts b/src/views/register.ts index 2ef23a0..8e84e4b 100644 --- a/src/views/register.ts +++ b/src/views/register.ts @@ -1,6 +1,6 @@ -import * as handlebars from "handlebars" +import * as handlebars from "handlebars"; import { readFileSync } from "fs"; -import { __ as i__ } from "i18n" +import { __ as i__ } from "i18n"; import config from "../config"; let template: handlebars.TemplateDelegate; @@ -11,11 +11,11 @@ function loadStatic() { export default function GetRegistrationPage(__: typeof i__): string { if (config.core.dev) { - loadStatic() + loadStatic(); } - let data = {} - return template(data, { helpers: { "i18n": __ } }) + let data = {}; + return template(data, { helpers: { i18n: __ } }); } -loadStatic() \ No newline at end of file +loadStatic(); diff --git a/tsconfig.json b/tsconfig.json index 86b1ec5..a04dceb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,17 @@ { "compilerOptions": { /* Basic Options */ - "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "declaration": true, /* Generates corresponding '.d.ts' file. */ - "sourceMap": true, /* Generates corresponding '.map' file. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - "strict": false, /* Enable all strict type-checking options. */ + "target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "declaration": true /* Generates corresponding '.d.ts' file. */, + "sourceMap": true /* Generates corresponding '.map' file. */, + "outDir": "./lib" /* Redirect output structure to the directory. */, + "strict": false /* Enable all strict type-checking options. */, "preserveWatchOutput": true, - "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, + "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ }, - "exclude": [ - "node_modules/" - ], - "files": [ - "src/express.d.ts" - ], - "include": [ - "./src" - ] -} \ No newline at end of file + "exclude": ["node_modules/"], + "files": ["src/express.d.ts"], + "include": ["./src"] +} diff --git a/views/build.js b/views/build.js index 6e0ef16..92e8232 100644 --- a/views/build.js +++ b/views/build.js @@ -5,49 +5,45 @@ const { copyFileSync, writeFileSync, readFileSync, - exists -} = require('fs') -const { - join, - basename, - dirname -} = require('path') + exists, +} = require("fs"); +const { join, basename, dirname } = require("path"); - - -const isDirectory = source => lstatSync(source).isDirectory() -const getDirectories = source => - readdirSync(source).map(name => join(source, name)).filter(isDirectory) +const isDirectory = (source) => lstatSync(source).isDirectory(); +const getDirectories = (source) => + readdirSync(source) + .map((name) => join(source, name)) + .filter(isDirectory); function ensureDir(folder) { try { - if (!isDirectory(folder)) mkdirSync(folder) + if (!isDirectory(folder)) mkdirSync(folder); } catch (e) { - mkdirSync(folder) + mkdirSync(folder); } } +const fileExists = (filename) => + new Promise((yes, no) => exists(filename, (exi) => yes(exi))); +ensureDir("./out"); -const fileExists = (filename) => new Promise((yes, no) => exists(filename, (exi) => yes(exi))); -ensureDir("./out") - -const sass = require('sass'); +const sass = require("sass"); function findHead(elm) { if (elm.tagName === "head") return elm; for (let i = 0; i < elm.childNodes.length; i++) { - let res = findHead(elm.childNodes[i]) + let res = findHead(elm.childNodes[i]); if (res) return res; } return undefined; } -const rollup = require("rollup") -const includepaths = require("rollup-plugin-includepaths") +const rollup = require("rollup"); +const includepaths = require("rollup-plugin-includepaths"); const typescript = require("rollup-plugin-typescript2"); const resolve = require("rollup-plugin-node-resolve"); -const minify = require("html-minifier").minify -const gzipSize = require('gzip-size'); +const minify = require("html-minifier").minify; +const gzipSize = require("gzip-size"); async function file_name(folder, name, exts) { for (let ext of exts) { @@ -61,58 +57,57 @@ async function buildPage(folder) { const pagename = basename(folder); const outpath = "./out/" + pagename; - ensureDir(outpath) + ensureDir(outpath); const basefile = await file_name(folder, pagename, ["tsx", "ts", "js"]); - let bundle = await rollup.rollup({ input: basefile, plugins: [ includepaths({ - paths: ["shared", "node_modules"] + paths: ["shared", "node_modules"], }), typescript(), resolve({ // not all files you want to resolve are .js files - extensions: ['.mjs', '.js', '.jsx', '.json'], // Default: [ '.mjs', '.js', '.json', '.node' ] + extensions: [".mjs", ".js", ".jsx", ".json"], // Default: [ '.mjs', '.js', '.json', '.node' ] // whether to prefer built-in modules (e.g. `fs`, `path`) or // local ones with the same names - preferBuiltins: false, // Default: true - }) + preferBuiltins: false, // Default: true + }), ], - treeshake: true - }) + treeshake: true, + }); let { output } = await bundle.generate({ format: "iife", - compact: true - }) + compact: true, + }); let { code } = output[0]; let sass_res = sass.renderSync({ file: folder + `/${pagename}.scss`, includePaths: ["./node_modules", folder, "./shared"], - outputStyle: "compressed" - }) + outputStyle: "compressed", + }); let css = "\n"; let script = "\n"; let html = readFileSync(`${folder}/${pagename}.hbs`).toString("utf8"); - let idx = html.indexOf("") - if (idx < 0) throw new Error("No head element found") - let idx2 = html.indexOf("") - if (idx2 < 0) throw new Error("No body element found") + let idx = html.indexOf(""); + if (idx < 0) throw new Error("No head element found"); + let idx2 = html.indexOf(""); + if (idx2 < 0) throw new Error("No body element found"); if (idx < idx2) { - let part1 = html.slice(0, idx) + let part1 = html.slice(0, idx); let part2 = html.slice(idx, idx2); let part3 = html.slice(idx2, html.length); html = part1 + css + part2 + script + part3; } else { - let part1 = html.slice(0, idx2) + let part1 = html.slice(0, idx2); let part2 = html.slice(idx2, idx); let part3 = html.slice(idx, html.length); html = part1 + script + part2 + css + part3; @@ -126,45 +121,50 @@ async function buildPage(folder) { minifyCSS: false, minifyJS: false, removeComments: true, - useShortDoctype: true - }) + useShortDoctype: true, + }); - let gzips = await gzipSize(result) - writeFileSync(`${outpath}/${pagename}.html`, result) + let gzips = await gzipSize(result); + writeFileSync(`${outpath}/${pagename}.html`, result); let stats = { sass: sass_res.stats, js: { - chars: code.length + chars: code.length, }, css: { - chars: css.length + chars: css.length, }, bundle_size: result.length, - gzip_size: gzips - } + gzip_size: gzips, + }; - writeFileSync(outpath + `/stats.json`, JSON.stringify(stats, null, " ")) + writeFileSync(outpath + `/stats.json`, JSON.stringify(stats, null, " ")); } async function run() { console.log("Start compiling!"); let pages = getDirectories("./src"); - await Promise.all(pages.map(async e => { - try { - await buildPage(e) - } catch (er) { - console.error("Failed compiling", basename(e)) - console.log(er) - } - })) - console.log("Finished compiling!") + await Promise.all( + pages.map(async (e) => { + try { + await buildPage(e); + } catch (er) { + console.error("Failed compiling", basename(e)); + console.log(er); + } + }) + ); + console.log("Finished compiling!"); } - const chokidar = require("chokidar"); if (process.argv.join(" ").toLowerCase().indexOf("watch") >= 0) - chokidar.watch(["./src", "./node_modules", "./package.json", "./package-lock.json"], { - ignoreInitial: true - }) + chokidar + .watch( + ["./src", "./node_modules", "./package.json", "./package-lock.json"], + { + ignoreInitial: true, + } + ) .on("all", () => run()); -run() +run(); diff --git a/views/shared/cookie.js b/views/shared/cookie.js index 0afcb84..6c1d2b4 100644 --- a/views/shared/cookie.js +++ b/views/shared/cookie.js @@ -1,15 +1,15 @@ export function setCookie(cname, cvalue, exdate) { var expires = exdate ? `;expires=${exdate}` : ""; - document.cookie = `${cname}=${cvalue}${expires}` + document.cookie = `${cname}=${cvalue}${expires}`; } export function getCookie(cname) { var name = cname + "="; var decodedCookie = decodeURIComponent(document.cookie); - var ca = decodedCookie.split(';'); + var ca = decodedCookie.split(";"); for (var i = 0; i < ca.length; i++) { var c = ca[i]; - while (c.charAt(0) == ' ') { + while (c.charAt(0) == " ") { c = c.substring(1); } if (c.indexOf(name) == 0) { @@ -17,4 +17,4 @@ export function getCookie(cname) { } } return ""; -} \ No newline at end of file +} diff --git a/views/shared/event.js b/views/shared/event.js index f38440a..2191bec 100644 --- a/views/shared/event.js +++ b/views/shared/event.js @@ -2,12 +2,11 @@ export default function fireEvent(element, event) { if (document.createEventObject) { // dispatch for IE var evt = document.createEventObject(); - return element.fireEvent('on' + event, evt) - } - else { + return element.fireEvent("on" + event, evt); + } else { // dispatch for firefox + others var evt = document.createEvent("HTMLEvents"); evt.initEvent(event, true, true); // event type,bubbling,cancelable return !element.dispatchEvent(evt); } -} \ No newline at end of file +} diff --git a/views/shared/formdata.js b/views/shared/formdata.js index 71a686d..609e502 100644 --- a/views/shared/formdata.js +++ b/views/shared/formdata.js @@ -1,14 +1,18 @@ export default function getFormData(element) { let data = {}; - if (element.name !== undefined && element.name !== null && element.name !== "") { + if ( + element.name !== undefined && + element.name !== null && + element.name !== "" + ) { if (typeof element.name === "string") { if (element.type === "checkbox") data[element.name] = element.checked; else data[element.name] = element.value; } } - element.childNodes.forEach(child => { + element.childNodes.forEach((child) => { let res = getFormData(child); data = Object.assign(data, res); - }) + }); return data; -} \ No newline at end of file +} diff --git a/views/shared/inputs.js b/views/shared/inputs.js index d185cb1..819d204 100644 --- a/views/shared/inputs.js +++ b/views/shared/inputs.js @@ -1,24 +1,24 @@ (() => { const run = () => { - document.querySelectorAll(".floating>input").forEach(e => { + document.querySelectorAll(".floating>input").forEach((e) => { function checkState() { if (e.value !== "") { if (e.classList.contains("used")) return; - e.classList.add("used") + e.classList.add("used"); } else { - if (e.classList.contains("used")) e.classList.remove("used") + if (e.classList.contains("used")) e.classList.remove("used"); } } - e.addEventListener("change", () => checkState()) - checkState() - }) - } + e.addEventListener("change", () => checkState()); + checkState(); + }); + }; run(); var mutationObserver = new MutationObserver(() => { - run() + run(); }); mutationObserver.observe(document.documentElement, { @@ -28,6 +28,6 @@ subtree: true, }); - window.Mutt - window.addEventListener("DOMNodeInserted", () => run()) -})(); \ No newline at end of file + window.Mutt; + window.addEventListener("DOMNodeInserted", () => run()); +})(); diff --git a/views/shared/inputs.scss b/views/shared/inputs.scss index f6dbbdf..9fa5a21 100644 --- a/views/shared/inputs.scss +++ b/views/shared/inputs.scss @@ -5,7 +5,7 @@ min-height: 45px; } -.floating>input { +.floating > input { font-size: 18px; padding: 10px 10px 10px 5px; appearance: none; @@ -19,13 +19,13 @@ border-bottom: 1px solid #757575; } -.floating>input:focus { +.floating > input:focus { outline: none; } /* Label */ -.floating>label { +.floating > label { color: #999; font-size: 18px; font-weight: normal; @@ -38,10 +38,10 @@ /* active */ -.floating>input:focus~label, -.floating>input.used~label { - top: -.75em; - transform: scale(.75); +.floating > input:focus ~ label, +.floating > input.used ~ label { + top: -0.75em; + transform: scale(0.75); left: -2px; /* font-size: 14px; */ color: $primary; @@ -58,7 +58,7 @@ .bar:before, .bar:after { - content: ''; + content: ""; height: 2px; width: 0; bottom: 1px; @@ -77,8 +77,8 @@ /* active */ -.floating>input:focus~.bar:before, -.floating>input:focus~.bar:after { +.floating > input:focus ~ .bar:before, +.floating > input:focus ~ .bar:after { width: 50%; } @@ -96,7 +96,7 @@ /* active */ -.floating>input:focus~.highlight { +.floating > input:focus ~ .highlight { animation: inputHighlighter 0.3s ease; } @@ -110,4 +110,4 @@ width: 0; background: transparent; } -} \ No newline at end of file +} diff --git a/views/shared/mat_bs.scss b/views/shared/mat_bs.scss index 03affb8..30a5aac 100644 --- a/views/shared/mat_bs.scss +++ b/views/shared/mat_bs.scss @@ -1,2 +1,2 @@ @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons"); -@import url("https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"); \ No newline at end of file +@import url("https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"); diff --git a/views/shared/preact.min.js b/views/shared/preact.min.js deleted file mode 100644 index 773e540..0000000 --- a/views/shared/preact.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(){"use strict";function e(e,t){var n,o,r,i,l=W;for(i=arguments.length;i-- >2;)P.push(arguments[i]);t&&null!=t.children&&(P.length||P.push(t.children),delete t.children);while(P.length)if((o=P.pop())&&void 0!==o.pop)for(i=o.length;i--;)P.push(o[i]);else"boolean"==typeof o&&(o=null),(r="function"!=typeof e)&&(null==o?o="":"number"==typeof o?o+="":"string"!=typeof o&&(r=!1)),r&&n?l[l.length-1]+=o:l===W?l=[o]:l.push(o),n=r;var a=new T;return a.nodeName=e,a.children=l,a.attributes=null==t?void 0:t,a.key=null==t?void 0:t.key,void 0!==M.vnode&&M.vnode(a),a}function t(e,t){for(var n in t)e[n]=t[n];return e}function n(e,t){null!=e&&("function"==typeof e?e(t):e.current=t)}function o(n,o){return e(n.nodeName,t(t({},n.attributes),o),arguments.length>2?[].slice.call(arguments,2):n.children)}function r(e){!e.__d&&(e.__d=!0)&&1==V.push(e)&&(M.debounceRendering||D)(i)}function i(){var e;while(e=V.pop())e.__d&&x(e)}function l(e,t,n){return"string"==typeof t||"number"==typeof t?void 0!==e.splitText:"string"==typeof t.nodeName?!e._componentConstructor&&a(e,t.nodeName):n||e._componentConstructor===t.nodeName}function a(e,t){return e.__n===t||e.nodeName.toLowerCase()===t.toLowerCase()}function u(e){var n=t({},e.attributes);n.children=e.children;var o=e.nodeName.defaultProps;if(void 0!==o)for(var r in o)void 0===n[r]&&(n[r]=o[r]);return n}function c(e,t){var n=t?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n.__n=e,n}function p(e){var t=e.parentNode;t&&t.removeChild(e)}function s(e,t,o,r,i){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)n(o,null),n(r,e);else if("class"!==t||i)if("style"===t){if(r&&"string"!=typeof r&&"string"!=typeof o||(e.style.cssText=r||""),r&&"object"==typeof r){if("string"!=typeof o)for(var l in o)l in r||(e.style[l]="");for(var l in r)e.style[l]="number"==typeof r[l]&&!1===E.test(l)?r[l]+"px":r[l]}}else if("dangerouslySetInnerHTML"===t)r&&(e.innerHTML=r.__html||"");else if("o"==t[0]&&"n"==t[1]){var a=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),r?o||e.addEventListener(t,_,a):e.removeEventListener(t,_,a),(e.__l||(e.__l={}))[t]=r}else if("list"!==t&&"type"!==t&&!i&&t in e){try{e[t]=null==r?"":r}catch(e){}null!=r&&!1!==r||"spellcheck"==t||e.removeAttribute(t)}else{var u=i&&t!==(t=t.replace(/^xlink:?/,""));null==r||!1===r?u?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof r&&(u?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),r):e.setAttribute(t,r))}else e.className=r||""}function _(e){return this.__l[e.type](M.event&&M.event(e)||e)}function f(){var e;while(e=A.shift())M.afterMount&&M.afterMount(e),e.componentDidMount&&e.componentDidMount()}function d(e,t,n,o,r,i){H++||(R=null!=r&&void 0!==r.ownerSVGElement,B=null!=e&&!("__preactattr_"in e));var l=h(e,t,n,o,i);return r&&l.parentNode!==r&&r.appendChild(l),--H||(B=!1,i||f()),l}function h(e,t,n,o,r){var i=e,l=R;if(null!=t&&"boolean"!=typeof t||(t=""),"string"==typeof t||"number"==typeof t)return e&&void 0!==e.splitText&&e.parentNode&&(!e._component||r)?e.nodeValue!=t&&(e.nodeValue=t):(i=document.createTextNode(t),e&&(e.parentNode&&e.parentNode.replaceChild(i,e),v(e,!0))),i.__preactattr_=!0,i;var u=t.nodeName;if("function"==typeof u)return N(e,t,n,o);if(R="svg"===u||"foreignObject"!==u&&R,u+="",(!e||!a(e,u))&&(i=c(u,R),e)){while(e.firstChild)i.appendChild(e.firstChild);e.parentNode&&e.parentNode.replaceChild(i,e),v(e,!0)}var p=i.firstChild,s=i.__preactattr_,_=t.children;if(null==s){s=i.__preactattr_={};for(var f=i.attributes,d=f.length;d--;)s[f[d].name]=f[d].value}return!B&&_&&1===_.length&&"string"==typeof _[0]&&null!=p&&void 0!==p.splitText&&null==p.nextSibling?p.nodeValue!=_[0]&&(p.nodeValue=_[0]):(_&&_.length||null!=p)&&m(i,_,n,o,B||null!=s.dangerouslySetInnerHTML),y(i,t.attributes,s),R=l,i}function m(e,t,n,o,r){var i,a,u,c,s,_=e.childNodes,f=[],d={},m=0,b=0,y=_.length,g=0,w=t?t.length:0;if(0!==y)for(var C=0;C { - if (e.status !== 200) throw new Error(await e.text() || e.statusText); - return e.json() - }).then(e => { - if (e.error) return Promise.reject(new Error(typeof e.error === "string" ? e.error : JSON.stringify(e.error))); - return e; + credentials: "include", }) -} \ No newline at end of file + .then(async (e) => { + if (e.status !== 200) + throw new Error((await e.text()) || e.statusText); + return e.json(); + }) + .then((e) => { + if (e.error) + return Promise.reject( + new Error( + typeof e.error === "string" + ? e.error + : JSON.stringify(e.error) + ) + ); + return e; + }); +} diff --git a/views/shared/sha512.js b/views/shared/sha512.js index b64984e..a47137f 100644 --- a/views/shared/sha512.js +++ b/views/shared/sha512.js @@ -1 +1,484 @@ -var b;if(!(b=t)){var w=Math,y={},B=y.p={},aa=function(){},C=B.A={extend:function(o){aa.prototype=this;var _=new aa;return o&&_.u(o),_.z=this,_},create:function(){var o=this.extend();return o.h.apply(o,arguments),o},h:function(){},u:function(o){for(var _ in o)o.hasOwnProperty(_)&&(this[_]=o[_]);o.hasOwnProperty("toString")&&(this.toString=o.toString)},e:function(){return this.z.extend(this)}},D=B.i=C.extend({h:function(o,_){o=this.d=o||[],this.c=void 0==_?4*o.length:_},toString:function(o){return(o||ba).stringify(this)},concat:function(o){var _=this.d,Da=o.d,Ea=this.c,o=o.c;if(this.t(),Ea%4)for(var Fa=0;Fa>>2]|=(255&Da[Fa>>>2]>>>24-8*(Fa%4))<<24-8*((Ea+Fa)%4);else if(65535>>2]=Da[Fa>>>2];else _.push.apply(_,Da);return this.c+=o,this},t:function(){var o=this.d,_=this.c;o[_>>>2]&=4294967295<<32-8*(_%4),o.length=w.ceil(_/4)},e:function(){var o=C.e.call(this);return o.d=this.d.slice(0),o},random:function(o){for(var _=[],Da=0;Da>>2]>>>24-8*(Ea%4),Da.push((Fa>>>4).toString(16)),Da.push((15&Fa).toString(16));return Da.join("")},parse:function(o){for(var _=o.length,Da=[],Ea=0;Ea<_;Ea+=2)Da[Ea>>>3]|=parseInt(o.substr(Ea,2),16)<<24-4*(Ea%8);return D.create(Da,_/2)}},da=ca.M={stringify:function(o){for(var _=o.d,o=o.c,Da=[],Ea=0;Ea>>2]>>>24-8*(Ea%4)));return Da.join("")},parse:function(o){for(var _=o.length,Da=[],Ea=0;Ea<_;Ea++)Da[Ea>>>2]|=(255&o.charCodeAt(Ea))<<24-8*(Ea%4);return D.create(Da,_)}},ea=ca.N={stringify:function(o){try{return decodeURIComponent(escape(da.stringify(o)))}catch(_){throw Error("Malformed UTF-8 data")}},parse:function(o){return da.parse(unescape(encodeURIComponent(o)))}},ia=B.I=C.extend({reset:function(){this.g=D.create(),this.j=0},l:function(o){"string"==typeof o&&(o=ea.parse(o)),this.g.concat(o),this.j+=o.c},m:function(o){var _=this.g,Da=_.d,Ea=_.c,Fa=this.n,Ga=Ea/(4*Fa),Ga=o?w.ceil(Ga):w.max((0|Ga)-this.r,0),o=Ga*Fa,Ea=w.min(4*o,Ea);if(o){for(var Ha=0;HaAa;Aa++)$[Aa]=L();M=M.k=xa.extend({q:function(){this.f=ya.create([L(1779033703,4089235720),L(3144134277,2227873595),L(1013904242,4271175723),L(2773480762,1595750129),L(1359893119,2917565137),L(2600822924,725511199),L(528734635,4215389547),L(1541459225,327033209)])},H:function(o,_){for(var qb,Da=this.f.d,Ea=Da[0],Fa=Da[1],Ga=Da[2],Ha=Da[3],Ia=Da[4],Ja=Da[5],Ka=Da[6],Da=Da[7],La=Ea.a,Ma=Ea.b,Na=Fa.a,Oa=Fa.b,Pa=Ga.a,Qa=Ga.b,Ra=Ha.a,Sa=Ha.b,Ta=Ia.a,Ua=Ia.b,Va=Ja.a,Wa=Ja.b,Xa=Ka.a,Ya=Ka.b,Za=Da.a,$a=Da.b,_a=La,ab=Ma,bb=Na,cb=Oa,db=Pa,eb=Qa,fb=Ra,gb=Sa,hb=Ta,ib=Ua,jb=Va,kb=Wa,lb=Xa,mb=Ya,nb=Za,ob=$a,pb=0;80>pb;pb++){if(qb=$[pb],16>pb)var rb=qb.a=0|o[_+2*pb],sb=qb.b=0|o[_+2*pb+1];else{var rb=$[pb-15],sb=rb.a,tb=rb.b,rb=(tb<<31|sb>>>1)^(tb<<24|sb>>>8)^sb>>>7,tb=(sb<<31|tb>>>1)^(sb<<24|tb>>>8)^(sb<<25|tb>>>7),ub=$[pb-2],sb=ub.a,vb=ub.b,ub=(vb<<13|sb>>>19)^(sb<<3|vb>>>29)^sb>>>6,vb=(sb<<13|vb>>>19)^(vb<<3|sb>>>29)^(sb<<26|vb>>>6),sb=$[pb-7],wb=sb.a,xb=$[pb-16],yb=xb.a,xb=xb.b,sb=tb+sb.b,rb=rb+wb+(sb>>>0>>0?1:0),sb=sb+vb,rb=rb+ub+(sb>>>0>>0?1:0),sb=sb+xb,rb=rb+yb+(sb>>>0>>0?1:0);qb.a=rb,qb.b=sb}var wb=hb&jb^~hb&lb,xb=ib&kb^~ib&mb,qb=_a&bb^_a&db^bb&db,tb=(ab<<4|_a>>>28)^(_a<<30|ab>>>2)^(_a<<25|ab>>>7),ub=(_a<<4|ab>>>28)^(ab<<30|_a>>>2)^(ab<<25|_a>>>7),vb=za[pb],Ab=vb.a,Bb=vb.b,vb=ob+((hb<<18|ib>>>14)^(hb<<14|ib>>>18)^(ib<<23|hb>>>9)),yb=nb+((ib<<18|hb>>>14)^(ib<<14|hb>>>18)^(hb<<23|ib>>>9))+(vb>>>0>>0?1:0),vb=vb+xb,yb=yb+wb+(vb>>>0>>0?1:0),vb=vb+Bb,yb=yb+Ab+(vb>>>0>>0?1:0),vb=vb+sb,yb=yb+rb+(vb>>>0>>0?1:0),sb=ub+(ab&cb^ab&eb^cb&eb),qb=tb+qb+(sb>>>0>>0?1:0),nb=lb,ob=mb,lb=jb,mb=kb,jb=hb,kb=ib,ib=0|gb+vb,hb=0|fb+yb+(ib>>>0>>0?1:0),fb=db,gb=eb,db=bb,eb=cb,bb=_a,cb=ab,ab=0|vb+sb,_a=0|yb+qb+(ab>>>0>>0?1:0)}Ma=Ea.b=0|Ma+ab,Ea.a=0|La+_a+(Ma>>>0>>0?1:0),Oa=Fa.b=0|Oa+cb,Fa.a=0|Na+bb+(Oa>>>0>>0?1:0),Qa=Ga.b=0|Qa+eb,Ga.a=0|Pa+db+(Qa>>>0>>0?1:0),Sa=Ha.b=0|Sa+gb,Ha.a=0|Ra+fb+(Sa>>>0>>0?1:0),Ua=Ia.b=0|Ua+ib,Ia.a=0|Ta+hb+(Ua>>>0>>0?1:0),Wa=Ja.b=0|Wa+kb,Ja.a=0|Va+jb+(Wa>>>0>>0?1:0),Ya=Ka.b=0|Ya+mb,Ka.a=0|Xa+lb+(Ya>>>0>>0?1:0),$a=Da.b=0|$a+ob,Da.a=0|Za+nb+($a>>>0>>0?1:0)},G:function(){var o=this.g,_=o.d,Da=8*this.j,Ea=8*o.c;_[Ea>>>5]|=128<<24-Ea%32,_[(Ea+128>>>10<<5)+31]=Da,o.c=4*_.length,this.m(),this.f=this.f.v()},n:32}),t.k=xa.D(M),t.L=xa.F(M);export default function sha512 (o){return t.k(o)+""}; \ No newline at end of file +var b; +if (!(b = t)) { + var w = Math, + y = {}, + B = (y.p = {}), + aa = function () {}, + C = (B.A = { + extend: function (o) { + aa.prototype = this; + var _ = new aa(); + return o && _.u(o), (_.z = this), _; + }, + create: function () { + var o = this.extend(); + return o.h.apply(o, arguments), o; + }, + h: function () {}, + u: function (o) { + for (var _ in o) o.hasOwnProperty(_) && (this[_] = o[_]); + o.hasOwnProperty("toString") && (this.toString = o.toString); + }, + e: function () { + return this.z.extend(this); + }, + }), + D = (B.i = C.extend({ + h: function (o, _) { + (o = this.d = o || []), (this.c = void 0 == _ ? 4 * o.length : _); + }, + toString: function (o) { + return (o || ba).stringify(this); + }, + concat: function (o) { + var _ = this.d, + Da = o.d, + Ea = this.c, + o = o.c; + if ((this.t(), Ea % 4)) + for (var Fa = 0; Fa < o; Fa++) + _[(Ea + Fa) >>> 2] |= + (255 & (Da[Fa >>> 2] >>> (24 - 8 * (Fa % 4)))) << + (24 - 8 * ((Ea + Fa) % 4)); + else if (65535 < Da.length) + for (Fa = 0; Fa < o; Fa += 4) _[(Ea + Fa) >>> 2] = Da[Fa >>> 2]; + else _.push.apply(_, Da); + return (this.c += o), this; + }, + t: function () { + var o = this.d, + _ = this.c; + (o[_ >>> 2] &= 4294967295 << (32 - 8 * (_ % 4))), + (o.length = w.ceil(_ / 4)); + }, + e: function () { + var o = C.e.call(this); + return (o.d = this.d.slice(0)), o; + }, + random: function (o) { + for (var _ = [], Da = 0; Da < o; Da += 4) + _.push(0 | (4294967296 * w.random())); + return D.create(_, o); + }, + })), + ca = (y.O = {}), + ba = (ca.K = { + stringify: function (o) { + for (var Fa, _ = o.d, o = o.c, Da = [], Ea = 0; Ea < o; Ea++) + (Fa = 255 & (_[Ea >>> 2] >>> (24 - 8 * (Ea % 4)))), + Da.push((Fa >>> 4).toString(16)), + Da.push((15 & Fa).toString(16)); + return Da.join(""); + }, + parse: function (o) { + for (var _ = o.length, Da = [], Ea = 0; Ea < _; Ea += 2) + Da[Ea >>> 3] |= + parseInt(o.substr(Ea, 2), 16) << (24 - 4 * (Ea % 8)); + return D.create(Da, _ / 2); + }, + }), + da = (ca.M = { + stringify: function (o) { + for (var _ = o.d, o = o.c, Da = [], Ea = 0; Ea < o; Ea++) + Da.push( + String.fromCharCode( + 255 & (_[Ea >>> 2] >>> (24 - 8 * (Ea % 4))) + ) + ); + return Da.join(""); + }, + parse: function (o) { + for (var _ = o.length, Da = [], Ea = 0; Ea < _; Ea++) + Da[Ea >>> 2] |= (255 & o.charCodeAt(Ea)) << (24 - 8 * (Ea % 4)); + return D.create(Da, _); + }, + }), + ea = (ca.N = { + stringify: function (o) { + try { + return decodeURIComponent(escape(da.stringify(o))); + } catch (_) { + throw Error("Malformed UTF-8 data"); + } + }, + parse: function (o) { + return da.parse(unescape(encodeURIComponent(o))); + }, + }), + ia = (B.I = C.extend({ + reset: function () { + (this.g = D.create()), (this.j = 0); + }, + l: function (o) { + "string" == typeof o && (o = ea.parse(o)), + this.g.concat(o), + (this.j += o.c); + }, + m: function (o) { + var _ = this.g, + Da = _.d, + Ea = _.c, + Fa = this.n, + Ga = Ea / (4 * Fa), + Ga = o ? w.ceil(Ga) : w.max((0 | Ga) - this.r, 0), + o = Ga * Fa, + Ea = w.min(4 * o, Ea); + if (o) { + for (var Ha = 0; Ha < o; Ha += Fa) this.H(Da, Ha); + (Ha = Da.splice(0, o)), (_.c -= Ea); + } + return D.create(Ha, Ea); + }, + e: function () { + var o = C.e.call(this); + return (o.g = this.g.e()), o; + }, + r: 0, + })); + B.B = ia.extend({ + h: function () { + this.reset(); + }, + reset: function () { + ia.reset.call(this), this.q(); + }, + update: function (o) { + return this.l(o), this.m(), this; + }, + o: function (o) { + return o && this.l(o), this.G(), this.f; + }, + e: function () { + var o = ia.e.call(this); + return (o.f = this.f.e()), o; + }, + n: 16, + D: function (o) { + return function (_, Da) { + return o.create(Da).o(_); + }; + }, + F: function (o) { + return function (_, Da) { + return ja.J.create(o, Da).o(_); + }; + }, + }); + var ja = (y.s = {}); + b = y; +} +var t = b, + K = t, + ka = K.p, + la = ka.A, + va = ka.i, + K = (K.w = {}); +(K.C = la.extend({ + h: function (o, _) { + (this.a = o), (this.b = _); + }, +})), + (K.i = la.extend({ + h: function (o, _) { + (o = this.d = o || []), (this.c = void 0 == _ ? 8 * o.length : _); + }, + v: function () { + for (var Fa, o = this.d, _ = o.length, Da = [], Ea = 0; Ea < _; Ea++) + (Fa = o[Ea]), Da.push(Fa.a), Da.push(Fa.b); + return va.create(Da, this.c); + }, + e: function () { + for ( + var o = la.e.call(this), + _ = (o.d = this.d.slice(0)), + Da = _.length, + Ea = 0; + Ea < Da; + Ea++ + ) + _[Ea] = _[Ea].e(); + return o; + }, + })); +function L() { + return wa.create.apply(wa, arguments); +} +for ( + var xa = t.p.B, + M = t.w, + wa = M.C, + ya = M.i, + M = t.s, + za = [ + L(1116352408, 3609767458), + L(1899447441, 602891725), + L(3049323471, 3964484399), + L(3921009573, 2173295548), + L(961987163, 4081628472), + L(1508970993, 3053834265), + L(2453635748, 2937671579), + L(2870763221, 3664609560), + L(3624381080, 2734883394), + L(310598401, 1164996542), + L(607225278, 1323610764), + L(1426881987, 3590304994), + L(1925078388, 4068182383), + L(2162078206, 991336113), + L(2614888103, 633803317), + L(3248222580, 3479774868), + L(3835390401, 2666613458), + L(4022224774, 944711139), + L(264347078, 2341262773), + L(604807628, 2007800933), + L(770255983, 1495990901), + L(1249150122, 1856431235), + L(1555081692, 3175218132), + L(1996064986, 2198950837), + L(2554220882, 3999719339), + L(2821834349, 766784016), + L(2952996808, 2566594879), + L(3210313671, 3203337956), + L(3336571891, 1034457026), + L(3584528711, 2466948901), + L(113926993, 3758326383), + L(338241895, 168717936), + L(666307205, 1188179964), + L(773529912, 1546045734), + L(1294757372, 1522805485), + L(1396182291, 2643833823), + L(1695183700, 2343527390), + L(1986661051, 1014477480), + L(2177026350, 1206759142), + L(2456956037, 344077627), + L(2730485921, 1290863460), + L(2820302411, 3158454273), + L(3259730800, 3505952657), + L(3345764771, 106217008), + L(3516065817, 3606008344), + L(3600352804, 1432725776), + L(4094571909, 1467031594), + L(275423344, 851169720), + L(430227734, 3100823752), + L(506948616, 1363258195), + L(659060556, 3750685593), + L(883997877, 3785050280), + L(958139571, 3318307427), + L(1322822218, 3812723403), + L(1537002063, 2003034995), + L(1747873779, 3602036899), + L(1955562222, 1575990012), + L(2024104815, 1125592928), + L(2227730452, 2716904306), + L(2361852424, 442776044), + L(2428436474, 593698344), + L(2756734187, 3733110249), + L(3204031479, 2999351573), + L(3329325298, 3815920427), + L(3391569614, 3928383900), + L(3515267271, 566280711), + L(3940187606, 3454069534), + L(4118630271, 4000239992), + L(116418474, 1914138554), + L(174292421, 2731055270), + L(289380356, 3203993006), + L(460393269, 320620315), + L(685471733, 587496836), + L(852142971, 1086792851), + L(1017036298, 365543100), + L(1126000580, 2618297676), + L(1288033470, 3409855158), + L(1501505948, 4234509866), + L(1607167915, 987167468), + L(1816402316, 1246189591), + ], + $ = [], + Aa = 0; + 80 > Aa; + Aa++ +) + $[Aa] = L(); +(M = M.k = xa.extend({ + q: function () { + this.f = ya.create([ + L(1779033703, 4089235720), + L(3144134277, 2227873595), + L(1013904242, 4271175723), + L(2773480762, 1595750129), + L(1359893119, 2917565137), + L(2600822924, 725511199), + L(528734635, 4215389547), + L(1541459225, 327033209), + ]); + }, + H: function (o, _) { + for ( + var qb, + Da = this.f.d, + Ea = Da[0], + Fa = Da[1], + Ga = Da[2], + Ha = Da[3], + Ia = Da[4], + Ja = Da[5], + Ka = Da[6], + Da = Da[7], + La = Ea.a, + Ma = Ea.b, + Na = Fa.a, + Oa = Fa.b, + Pa = Ga.a, + Qa = Ga.b, + Ra = Ha.a, + Sa = Ha.b, + Ta = Ia.a, + Ua = Ia.b, + Va = Ja.a, + Wa = Ja.b, + Xa = Ka.a, + Ya = Ka.b, + Za = Da.a, + $a = Da.b, + _a = La, + ab = Ma, + bb = Na, + cb = Oa, + db = Pa, + eb = Qa, + fb = Ra, + gb = Sa, + hb = Ta, + ib = Ua, + jb = Va, + kb = Wa, + lb = Xa, + mb = Ya, + nb = Za, + ob = $a, + pb = 0; + 80 > pb; + pb++ + ) { + if (((qb = $[pb]), 16 > pb)) + var rb = (qb.a = 0 | o[_ + 2 * pb]), + sb = (qb.b = 0 | o[_ + 2 * pb + 1]); + else { + var rb = $[pb - 15], + sb = rb.a, + tb = rb.b, + rb = + ((tb << 31) | (sb >>> 1)) ^ + ((tb << 24) | (sb >>> 8)) ^ + (sb >>> 7), + tb = + ((sb << 31) | (tb >>> 1)) ^ + ((sb << 24) | (tb >>> 8)) ^ + ((sb << 25) | (tb >>> 7)), + ub = $[pb - 2], + sb = ub.a, + vb = ub.b, + ub = + ((vb << 13) | (sb >>> 19)) ^ + ((sb << 3) | (vb >>> 29)) ^ + (sb >>> 6), + vb = + ((sb << 13) | (vb >>> 19)) ^ + ((vb << 3) | (sb >>> 29)) ^ + ((sb << 26) | (vb >>> 6)), + sb = $[pb - 7], + wb = sb.a, + xb = $[pb - 16], + yb = xb.a, + xb = xb.b, + sb = tb + sb.b, + rb = rb + wb + (sb >>> 0 < tb >>> 0 ? 1 : 0), + sb = sb + vb, + rb = rb + ub + (sb >>> 0 < vb >>> 0 ? 1 : 0), + sb = sb + xb, + rb = rb + yb + (sb >>> 0 < xb >>> 0 ? 1 : 0); + (qb.a = rb), (qb.b = sb); + } + var wb = (hb & jb) ^ (~hb & lb), + xb = (ib & kb) ^ (~ib & mb), + qb = (_a & bb) ^ (_a & db) ^ (bb & db), + tb = + ((ab << 4) | (_a >>> 28)) ^ + ((_a << 30) | (ab >>> 2)) ^ + ((_a << 25) | (ab >>> 7)), + ub = + ((_a << 4) | (ab >>> 28)) ^ + ((ab << 30) | (_a >>> 2)) ^ + ((ab << 25) | (_a >>> 7)), + vb = za[pb], + Ab = vb.a, + Bb = vb.b, + vb = + ob + + (((hb << 18) | (ib >>> 14)) ^ + ((hb << 14) | (ib >>> 18)) ^ + ((ib << 23) | (hb >>> 9))), + yb = + nb + + (((ib << 18) | (hb >>> 14)) ^ + ((ib << 14) | (hb >>> 18)) ^ + ((hb << 23) | (ib >>> 9))) + + (vb >>> 0 < ob >>> 0 ? 1 : 0), + vb = vb + xb, + yb = yb + wb + (vb >>> 0 < xb >>> 0 ? 1 : 0), + vb = vb + Bb, + yb = yb + Ab + (vb >>> 0 < Bb >>> 0 ? 1 : 0), + vb = vb + sb, + yb = yb + rb + (vb >>> 0 < sb >>> 0 ? 1 : 0), + sb = ub + ((ab & cb) ^ (ab & eb) ^ (cb & eb)), + qb = tb + qb + (sb >>> 0 < ub >>> 0 ? 1 : 0), + nb = lb, + ob = mb, + lb = jb, + mb = kb, + jb = hb, + kb = ib, + ib = 0 | (gb + vb), + hb = 0 | (fb + yb + (ib >>> 0 < gb >>> 0 ? 1 : 0)), + fb = db, + gb = eb, + db = bb, + eb = cb, + bb = _a, + cb = ab, + ab = 0 | (vb + sb), + _a = 0 | (yb + qb + (ab >>> 0 < vb >>> 0 ? 1 : 0)); + } + (Ma = Ea.b = 0 | (Ma + ab)), + (Ea.a = 0 | (La + _a + (Ma >>> 0 < ab >>> 0 ? 1 : 0))), + (Oa = Fa.b = 0 | (Oa + cb)), + (Fa.a = 0 | (Na + bb + (Oa >>> 0 < cb >>> 0 ? 1 : 0))), + (Qa = Ga.b = 0 | (Qa + eb)), + (Ga.a = 0 | (Pa + db + (Qa >>> 0 < eb >>> 0 ? 1 : 0))), + (Sa = Ha.b = 0 | (Sa + gb)), + (Ha.a = 0 | (Ra + fb + (Sa >>> 0 < gb >>> 0 ? 1 : 0))), + (Ua = Ia.b = 0 | (Ua + ib)), + (Ia.a = 0 | (Ta + hb + (Ua >>> 0 < ib >>> 0 ? 1 : 0))), + (Wa = Ja.b = 0 | (Wa + kb)), + (Ja.a = 0 | (Va + jb + (Wa >>> 0 < kb >>> 0 ? 1 : 0))), + (Ya = Ka.b = 0 | (Ya + mb)), + (Ka.a = 0 | (Xa + lb + (Ya >>> 0 < mb >>> 0 ? 1 : 0))), + ($a = Da.b = 0 | ($a + ob)), + (Da.a = 0 | (Za + nb + ($a >>> 0 < ob >>> 0 ? 1 : 0))); + }, + G: function () { + var o = this.g, + _ = o.d, + Da = 8 * this.j, + Ea = 8 * o.c; + (_[Ea >>> 5] |= 128 << (24 - (Ea % 32))), + (_[(((Ea + 128) >>> 10) << 5) + 31] = Da), + (o.c = 4 * _.length), + this.m(), + (this.f = this.f.v()); + }, + n: 32, +})), + (t.k = xa.D(M)), + (t.L = xa.F(M)); +export default function sha512(o) { + return t.k(o) + ""; +} diff --git a/views/shared/style.scss b/views/shared/style.scss index f18a7e7..4f2b192 100644 --- a/views/shared/style.scss +++ b/views/shared/style.scss @@ -1,7 +1,7 @@ // $primary: #4a89dc; -$primary: #1E88E5; +$primary: #1e88e5; $error: #ff2f00; .btn-primary { color: white !important; background-color: $primary !important; -} \ No newline at end of file +} diff --git a/views/src/admin/admin.js b/views/src/admin/admin.js index 619b2a3..1b99aa8 100644 --- a/views/src/admin/admin.js +++ b/views/src/admin/admin.js @@ -14,7 +14,7 @@ Handlebars.registerHelper("humangender", function (value, options) { } }); -// Deprecated since version 0.8.0 +// Deprecated since version 0.8.0 Handlebars.registerHelper("formatDate", function (datetime, format) { return new Date(datetime).toLocaleString(); }); @@ -26,9 +26,8 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { document.getElementById("sitename").innerText = title; } - - const cc = document.getElementById("custom_data") - const ccc = document.getElementById("custom_data_cont") + const cc = document.getElementById("custom_data"); + const ccc = document.getElementById("custom_data_cont"); function setCustomCard(content) { if (!content) { @@ -40,8 +39,8 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { } } - const error_cont = document.getElementById("error_cont") - const error_msg = document.getElementById("error_msg") + const error_cont = document.getElementById("error_cont"); + const error_msg = document.getElementById("error_msg"); function catchError(error) { error_cont.style.display = ""; @@ -50,40 +49,53 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { } async function renderUser() { - console.log("Rendering User") - setTitle("User") - const listt = Handlebars.compile(document.getElementById("template-user-list").innerText) + console.log("Rendering User"); + setTitle("User"); + const listt = Handlebars.compile( + document.getElementById("template-user-list").innerText + ); async function loadList() { let data = await request("/api/admin/user", "GET"); tableb.innerHTML = listt({ - users: data - }) + users: data, + }); } window.userOnChangeType = (id) => { - request("/api/admin/user?id=" + id, "PUT").then(() => loadList()).catch(catchError) - } + request("/api/admin/user?id=" + id, "PUT") + .then(() => loadList()) + .catch(catchError); + }; window.deleteUser = (id) => { - request("/api/admin/user?id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + request("/api/admin/user?id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; await loadList(); } async function renderPermissions(client_id, client_name) { - const listt = Handlebars.compile(document.getElementById("template-permission-list").innerText); - const formt = Handlebars.compile(document.getElementById("template-permission-form").innerText); + const listt = Handlebars.compile( + document.getElementById("template-permission-list").innerText + ); + const formt = Handlebars.compile( + document.getElementById("template-permission-form").innerText + ); setCustomCard(); async function loadList() { try { - let data = await request("/api/admin/permission?client=" + client_id, "GET"); + let data = await request( + "/api/admin/permission?client=" + client_id, + "GET" + ); tableb.innerHTML = listt({ client_id: client_id, client_name: client_name, - permissions: data - }) + permissions: data, + }); } catch (err) { catchError(err); } @@ -91,11 +103,13 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { window.gotoClients = () => { renderClient(); - } + }; window.deletePermission = (id) => { - request("/api/admin/permission?id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + request("/api/admin/permission?id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; window.createPermission = () => { try { @@ -103,41 +117,49 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { } catch (err) { console.log("Err", err); } - } - + }; window.createPermissionSubmit = (elm) => { console.log(elm); let data = getFormData(elm); console.log(data); - request("/api/admin/permission", "POST", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError) - } - await loadList() + request("/api/admin/permission", "POST", data) + .then(() => setCustomCard()) + .then(() => loadList()) + .catch(catchError); + }; + await loadList(); } async function renderClient() { - console.log("Rendering Client") - setTitle("Client") + console.log("Rendering Client"); + setTitle("Client"); - const listt = Handlebars.compile(document.getElementById("template-client-list").innerText) - const formt = Handlebars.compile(document.getElementById("template-client-form").innerText) + const listt = Handlebars.compile( + document.getElementById("template-client-list").innerText + ); + const formt = Handlebars.compile( + document.getElementById("template-client-form").innerText + ); let clients = []; async function loadList() { let data = await request("/api/admin/client", "GET"); clients = data; tableb.innerHTML = listt({ - clients: data - }) + clients: data, + }); } window.permissionsClient = (id) => { - renderPermissions(id, clients.find(e => e._id === id).name); - } + renderPermissions(id, clients.find((e) => e._id === id).name); + }; window.deleteClient = (id) => { - request("/api/admin/client/id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + request("/api/admin/client/id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; window.createClientSubmit = (elm) => { console.log(elm); @@ -146,45 +168,57 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { let id = data.id; delete data.id; if (id && id !== "") { - request("/api/admin/client?id=" + id, "PUT", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError) + request("/api/admin/client?id=" + id, "PUT", data) + .then(() => setCustomCard()) + .then(() => loadList()) + .catch(catchError); } else { - request("/api/admin/client", "POST", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError) + request("/api/admin/client", "POST", data) + .then(() => setCustomCard()) + .then(() => loadList()) + .catch(catchError); } - } + }; window.createClient = () => { setCustomCard(formt()); - } + }; window.editClient = (id) => { - let client = clients.find(e => e._id === id); - if (!client) return catchError(new Error("Client does not exist!!")) + let client = clients.find((e) => e._id === id); + if (!client) return catchError(new Error("Client does not exist!!")); setCustomCard(formt(client)); - } + }; await loadList().catch(catchError); } async function renderRegCode() { - console.log("Rendering RegCode") - setTitle("RegCode") + console.log("Rendering RegCode"); + setTitle("RegCode"); - const listt = Handlebars.compile(document.getElementById("template-regcode-list").innerText) + const listt = Handlebars.compile( + document.getElementById("template-regcode-list").innerText + ); async function loadList() { let data = await request("/api/admin/regcode", "GET"); tableb.innerHTML = listt({ - regcodes: data - }) + regcodes: data, + }); } window.deleteRegcode = (id) => { - request("/api/admin/regcode?id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + request("/api/admin/regcode?id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; window.createRegcode = () => { - request("/api/admin/regcode", "POST").then(() => loadList()).catch(catchError); - } + request("/api/admin/regcode", "POST") + .then(() => loadList()) + .catch(catchError); + }; await loadList().catch(catchError); } @@ -192,14 +226,14 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { const type = new URL(window.location.href).searchParams.get("type"); switch (type) { case "client": - renderClient().catch(catchError) - break + renderClient().catch(catchError); + break; case "regcode": - renderRegCode().catch(catchError) + renderRegCode().catch(catchError); break; case "user": default: renderUser().catch(catchError); break; } -})() \ No newline at end of file +})(); diff --git a/views/src/admin/admin.scss b/views/src/admin/admin.scss index 4d83cbd..60f08e0 100644 --- a/views/src/admin/admin.scss +++ b/views/src/admin/admin.scss @@ -100,4 +100,4 @@ table td { .col.form-group { padding-left: 0 !important; margin-left: 5px !important; -} \ No newline at end of file +} diff --git a/views/src/authorize/authorize.js b/views/src/authorize/authorize.js index 248682c..b1a19de 100644 --- a/views/src/authorize/authorize.js +++ b/views/src/authorize/authorize.js @@ -1,4 +1,6 @@ -document.getElementById("hidden_form").action += window.location.href.split("?")[1]; +document.getElementById("hidden_form").action += window.location.href.split( + "?" +)[1]; function submit() { document.getElementById("hidden_form").submit(); @@ -10,9 +12,10 @@ document.getElementById("cancel").onclick = () => { if (uri === "$local") { uri = "/code"; } - window.location.href = uri + "?error=access_denied&state=" + u.searchParams.get("state"); -} + window.location.href = + uri + "?error=access_denied&state=" + u.searchParams.get("state"); +}; document.getElementById("allow").onclick = () => { - submit() -} \ No newline at end of file + submit(); +}; diff --git a/views/src/authorize/authorize.scss b/views/src/authorize/authorize.scss index a2c1c13..131a0e4 100644 --- a/views/src/authorize/authorize.scss +++ b/views/src/authorize/authorize.scss @@ -10,29 +10,34 @@ hr { // display: block; // height: 1px; - border: 0; - border-top: 1px solid #b8b8b8; + border: 0; + border-top: 1px solid #b8b8b8; // margin: 1em 0; - // padding: 0; + // padding: 0; } body { - font-family: Helvetica; - background: #eee; - -webkit-font-smoothing: antialiased; + font-family: Helvetica; + background: #eee; + -webkit-font-smoothing: antialiased; } -.title { - text-align:center; +.title { + text-align: center; } -h1, h3 { font-weight: 300; } +h1, +h3 { + font-weight: 300; +} -h1 { color: #636363; } +h1 { + color: #636363; +} -ul { +ul { list-style: none; padding-left: 0; -} +} .permission { display: flex; @@ -70,10 +75,11 @@ ul { } .card { - max-width: 480px; - margin: 4em auto; - padding: 3em 2em 2em 2em; - background: #fafafa; - border: 1px solid #ebebeb; - box-shadow: rgba(0,0,0,0.14902) 0px 1px 1px 0px,rgba(0,0,0,0.09804) 0px 1px 2px 0px; -} \ No newline at end of file + max-width: 480px; + margin: 4em auto; + padding: 3em 2em 2em 2em; + background: #fafafa; + border: 1px solid #ebebeb; + box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, + rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px; +} diff --git a/views/src/login/login.js b/views/src/login/login.js index 292949d..89613d0 100644 --- a/views/src/login/login.js +++ b/views/src/login/login.js @@ -1,114 +1,123 @@ import sha from "sha512"; -import { - setCookie, - getCookie -} from "cookie" -import "inputs" +import { setCookie, getCookie } from "cookie"; +import "inputs"; -const loader = document.getElementById("loader") -const container = document.getElementById("container") -const usernameinput = document.getElementById("username") -const usernamegroup = document.getElementById("usernamegroup") -const uerrorfield = document.getElementById("uerrorfield") -const passwordinput = document.getElementById("password") -const passwordgroup = document.getElementById("passwordgroup") -const perrorfield = document.getElementById("perrorfield") -const nextbutton = document.getElementById("nextbutton") -const loginbutton = document.getElementById("loginbutton") +const loader = document.getElementById("loader"); +const container = document.getElementById("container"); +const usernameinput = document.getElementById("username"); +const usernamegroup = document.getElementById("usernamegroup"); +const uerrorfield = document.getElementById("uerrorfield"); +const passwordinput = document.getElementById("password"); +const passwordgroup = document.getElementById("passwordgroup"); +const perrorfield = document.getElementById("perrorfield"); +const nextbutton = document.getElementById("nextbutton"); +const loginbutton = document.getElementById("loginbutton"); let username; let salt; -usernameinput.focus() +usernameinput.focus(); const loading = () => { container.style.filter = "blur(2px)"; loader.style.display = ""; -} +}; const loading_fin = () => { - container.style.filter = "" + container.style.filter = ""; loader.style.display = "none"; -} +}; loading_fin(); usernameinput.onkeydown = (e) => { var keycode = e.keyCode ? e.keyCode : e.which; if (keycode === 13) nextbutton.click(); clearError(uerrorfield); -} +}; nextbutton.onclick = async () => { loading(); username = usernameinput.value; try { - let res = await fetch("/api/user/login?type=username&username=" + username, { - method: "POST" - }).then(e => { - if (e.status !== 200) throw new Error(e.statusText) - return e.json() - }).then(data => { - if (data.error) { - return Promise.reject(new Error(data.error)) + let res = await fetch( + "/api/user/login?type=username&username=" + username, + { + method: "POST", } - return data; - }) + ) + .then((e) => { + if (e.status !== 200) throw new Error(e.statusText); + return e.json(); + }) + .then((data) => { + if (data.error) { + return Promise.reject(new Error(data.error)); + } + return data; + }); salt = res.salt; - usernamegroup.classList.add("invisible") - passwordgroup.classList.remove("invisible") - passwordinput.focus() + usernamegroup.classList.add("invisible"); + passwordgroup.classList.remove("invisible"); + passwordinput.focus(); } catch (e) { - showError(uerrorfield, e.message) + showError(uerrorfield, e.message); } - loading_fin() -} + loading_fin(); +}; passwordinput.onkeydown = (e) => { var keycode = e.keyCode ? e.keyCode : e.which; if (keycode === 13) loginbutton.click(); clearError(perrorfield); -} +}; loginbutton.onclick = async () => { loading(); let pw = sha(salt + passwordinput.value); try { - let { login, special, tfa } = await fetch("/api/user/login?type=password", { - method: "POST", - body: JSON.stringify({ - username: usernameinput.value, - password: pw - }), - headers: { - 'content-type': 'application/json' - }, - }).then(e => { - if (e.status !== 200) throw new Error(e.statusText) - return e.json() - }).then(data => { - if (data.error) { - return Promise.reject(new Error(data.error)) + let { login, special, tfa } = await fetch( + "/api/user/login?type=password", + { + method: "POST", + body: JSON.stringify({ + username: usernameinput.value, + password: pw, + }), + headers: { + "content-type": "application/json", + }, } - return data; - }) + ) + .then((e) => { + if (e.status !== 200) throw new Error(e.statusText); + return e.json(); + }) + .then((data) => { + if (data.error) { + return Promise.reject(new Error(data.error)); + } + return data; + }); setCookie("login", login.token, new Date(login.expires).toUTCString()); - setCookie("special", special.token, new Date(special.expires).toUTCString()); - let d = new Date() - d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000)); //Keep the username 30 days + setCookie( + "special", + special.token, + new Date(special.expires).toUTCString() + ); + let d = new Date(); + d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days setCookie("username", username, d.toUTCString()); let url = new URL(window.location.href); - let state = url.searchParams.get("state") - let red = "/" + let state = url.searchParams.get("state"); + let red = "/"; if (tfa) twofactor(tfa); else { if (state) { - let base64 = url.searchParams.get("base64") - if (base64) - red = atob(state) - else - red = state + let base64 = url.searchParams.get("base64"); + if (base64) red = atob(state); + else red = state; } window.location.href = red; } @@ -117,19 +126,19 @@ loginbutton.onclick = async () => { showError(perrorfield, e.message); } loading_fin(); -} +}; function clearError(field) { field.innerText = ""; - field.classList.add("invisible") + field.classList.add("invisible"); } function showError(field, error) { field.innerText = error; - field.classList.remove("invisible") + field.classList.remove("invisible"); } -username = getCookie("username") +username = getCookie("username"); if (username) { usernameinput.value = username; @@ -138,10 +147,9 @@ if (username) { usernameinput.dispatchEvent(evt); } - function twofactor(tfa) { let list = tfa - .map(entry => { + .map((entry) => { switch (entry) { case 0: // OTC return "Authenticator App"; @@ -150,9 +158,9 @@ function twofactor(tfa) { } return undefined; }) - .filter(e => e !== undefined) + .filter((e) => e !== undefined) .reduce((p, c) => p + `
  • ${c}
  • `, ""); let tfl = document.getElementById("tflist"); tfl.innerHTML = list; -} \ No newline at end of file +} diff --git a/views/src/login/login.scss b/views/src/login/login.scss index 93323eb..f7f6ee8 100644 --- a/views/src/login/login.scss +++ b/views/src/login/login.scss @@ -41,7 +41,8 @@ form { padding: 3em 2em 2em 2em; background: #fafafa; border: 1px solid #ebebeb; - box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px; + box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, + rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px; position: relative; } @@ -50,35 +51,37 @@ form { height: 64px; margin: auto; position: absolute; - top: 0; left: 0; bottom: 0; right: 0; + top: 0; + left: 0; + bottom: 0; + right: 0; } -.loader{ - display: inline-block; - position: relative; - z-index: 100; +.loader { + display: inline-block; + position: relative; + z-index: 100; } .loader:after { - content: " "; - display: block; - width: 46px; - height: 46px; - margin: 1px; - border-radius: 50%; - border: 5px solid #000000; - border-color: #000000 transparent #000000 transparent; - animation: loader 1.2s linear infinite; + content: " "; + display: block; + width: 46px; + height: 46px; + margin: 1px; + border-radius: 50%; + border: 5px solid #000000; + border-color: #000000 transparent #000000 transparent; + animation: loader 1.2s linear infinite; } @keyframes loader { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } - footer { text-align: center; } @@ -86,13 +89,13 @@ footer { footer p { color: #888; font-size: 13px; - letter-spacing: .4px; + letter-spacing: 0.4px; } footer a { color: $primary; text-decoration: none; - transition: all .2s ease; + transition: all 0.2s ease; } footer a:hover { @@ -102,11 +105,11 @@ footer a:hover { footer img { width: 80px; - transition: all .2s ease; + transition: all 0.2s ease; } footer img:hover { - opacity: .83; + opacity: 0.83; } footer img:focus, diff --git a/views/src/login/login.tsx b/views/src/login/login.tsx index d0a3bd9..76fde9d 100644 --- a/views/src/login/login.tsx +++ b/views/src/login/login.tsx @@ -1,316 +1,434 @@ -import { h, Component, render } from "preact" -import "inputs" -import "./u2f-api-polyfill" +import { h, Component, render } from "preact"; +import "inputs"; +import "./u2f-api-polyfill"; import sha from "sha512"; -import { - setCookie, - getCookie -} from "cookie" +import { setCookie, getCookie } from "cookie"; let appname = "test"; function Loader() { - return
    -
    -
    + return ( +
    +
    +
    + ); } -class Username extends Component<{ username: string, onNext: (username: string, salt: string) => void }, { error: string, loading: boolean }> { - username_input: HTMLInputElement; - constructor() { - super(); - this.state = { error: undefined, loading: false } - } +class Username extends Component< + { username: string; onNext: (username: string, salt: string) => void }, + { error: string; loading: boolean } +> { + username_input: HTMLInputElement; + constructor() { + super(); + this.state = { error: undefined, loading: false }; + } - async onClick() { - this.setState({ loading: true }); - try { - let res = await fetch("/api/user/login?type=username&username=" + this.username_input.value, { - method: "POST" - }).then(e => { - if (e.status !== 200) throw new Error(e.statusText) - return e.json() - }).then(data => { - if (data.error) { - return Promise.reject(new Error(data.error)) - } - return data; + async onClick() { + this.setState({ loading: true }); + try { + let res = await fetch( + "/api/user/login?type=username&username=" + + this.username_input.value, + { + method: "POST", + } + ) + .then((e) => { + if (e.status !== 200) throw new Error(e.statusText); + return e.json(); }) - let salt = res.salt; - this.props.onNext(this.username_input.value, salt); - } catch (err) { - this.setState({ - error: err.message + .then((data) => { + if (data.error) { + return Promise.reject(new Error(data.error)); + } + return data; }); - } - this.setState({ loading: false }); - } + let salt = res.salt; + this.props.onNext(this.username_input.value, salt); + } catch (err) { + this.setState({ + error: err.message, + }); + } + this.setState({ loading: false }); + } - render() { - if (this.state.loading) return - return
    + render() { + if (this.state.loading) return ; + return ( +
    - { - let k = e.keyCode | e.which; - if (k === 13) this.onClick(); - this.setState({ error: undefined }) - }} type="text" value={this.username_input ? this.username_input.value : this.props.username} autofocus ref={elm => elm ? this.username_input = elm : undefined} /> - - - - {this.state.error ?
    {this.state.error}
    : undefined} + { + let k = e.keyCode | e.which; + if (k === 13) this.onClick(); + this.setState({ error: undefined }); + }} + type="text" + value={ + this.username_input + ? this.username_input.value + : this.props.username + } + autofocus + ref={(elm) => (elm ? (this.username_input = elm) : undefined)} + /> + + + + {this.state.error ? ( +
    {this.state.error}
    + ) : undefined}
    - -
    - } + +
    + ); + } } enum TFATypes { - OTC, - BACKUP_CODE, - YUBI_KEY, - APP_ALLOW + OTC, + BACKUP_CODE, + YUBI_KEY, + APP_ALLOW, } interface TwoFactors { - id: string; - name: string; - type: TFATypes; + id: string; + name: string; + type: TFATypes; } -class Password extends Component<{ username: string, salt: string, onNext: (login: Token, special: Token, tfa: TwoFactors[]) => void }, { error: string, loading: boolean }> { - password_input: HTMLInputElement; - constructor() { - super(); - this.state = { error: undefined, loading: false } - } +class Password extends Component< + { + username: string; + salt: string; + onNext: (login: Token, special: Token, tfa: TwoFactors[]) => void; + }, + { error: string; loading: boolean } +> { + password_input: HTMLInputElement; + constructor() { + super(); + this.state = { error: undefined, loading: false }; + } - async onClick() { - this.setState({ - loading: true - }); + async onClick() { + this.setState({ + loading: true, + }); - try { - let pw = sha(this.props.salt + this.password_input.value); - let { login, special, tfa } = await fetch("/api/user/login?type=password", { - method: "POST", - body: JSON.stringify({ - username: this.props.username, - password: pw - }), - headers: { - 'content-type': 'application/json' - }, - }).then(e => { - if (e.status !== 200) throw new Error(e.statusText) - return e.json() - }).then(data => { - if (data.error) { - return Promise.reject(new Error(data.error)) - } - return data; - }) - this.props.onNext(login, special, tfa); - } catch (err) { - this.setState({ error: err.messagae }); - } - this.setState({ loading: false }); - } - - render() { - if (this.state.loading) return - return
    -
    - { - let k = e.keyCode | e.which; - if (k === 13) this.onClick(); - this.setState({ error: undefined }) - }} type="password" ref={(elm: HTMLInputElement) => { - if (elm) { - this.password_input = elm - setTimeout(() => elm.focus(), 200) - // elm.focus(); - } - } - } /> - - - - {this.state.error ?
    {this.state.error}
    : undefined} -
    - -
    - } -} - -class TwoFactor extends Component<{ twofactors: TwoFactors[], next: (id: string, type: TFATypes) => void }, {}> { - render() { - let tfs = this.props.twofactors.map(fac => { - let name: string; - switch (fac.type) { - case TFATypes.OTC: - name = "Authenticator" - break; - case TFATypes.BACKUP_CODE: - name = "Backup code"; - break; - case TFATypes.APP_ALLOW: - name = "Use App: %s" - break; - case TFATypes.YUBI_KEY: - name = "Use Yubikey: %s" - break; + try { + let pw = sha(this.props.salt + this.password_input.value); + let { login, special, tfa } = await fetch( + "/api/user/login?type=password", + { + method: "POST", + body: JSON.stringify({ + username: this.props.username, + password: pw, + }), + headers: { + "content-type": "application/json", + }, } + ) + .then((e) => { + if (e.status !== 200) throw new Error(e.statusText); + return e.json(); + }) + .then((data) => { + if (data.error) { + return Promise.reject(new Error(data.error)); + } + return data; + }); + this.props.onNext(login, special, tfa); + } catch (err) { + this.setState({ error: err.messagae }); + } + this.setState({ loading: false }); + } - name = name.replace("%s", fac.name ? fac.name : ""); + render() { + if (this.state.loading) return ; + return ( +
    +
    + { + let k = e.keyCode | e.which; + if (k === 13) this.onClick(); + this.setState({ error: undefined }); + }} + type="password" + ref={(elm: HTMLInputElement) => { + if (elm) { + this.password_input = elm; + setTimeout(() => elm.focus(), 200); + // elm.focus(); + } + }} + /> + + + + {this.state.error ? ( +
    {this.state.error}
    + ) : undefined} +
    + +
    + ); + } +} - return
  • { - console.log("Click on Solution") - this.props.next(fac.id, fac.type) - }}> - {name} +class TwoFactor extends Component< + { twofactors: TwoFactors[]; next: (id: string, type: TFATypes) => void }, + {} +> { + render() { + let tfs = this.props.twofactors.map((fac) => { + let name: string; + switch (fac.type) { + case TFATypes.OTC: + name = "Authenticator"; + break; + case TFATypes.BACKUP_CODE: + name = "Backup code"; + break; + case TFATypes.APP_ALLOW: + name = "Use App: %s"; + break; + case TFATypes.YUBI_KEY: + name = "Use Yubikey: %s"; + break; + } + + name = name.replace("%s", fac.name ? fac.name : ""); + + return ( +
  • { + console.log("Click on Solution"); + this.props.next(fac.id, fac.type); + }} + > + {name}
  • - }) - return
    + ); + }); + return ( +

    Select one

    -
      - {tfs} -
    -
    - } +
      {tfs}
    +
    + ); + } } // class TFA_YubiKey extends Component<{ id: string, login: Token, special: Token, next: (login: Token, special: Token) => void }, {}> { -// render() { +// render() { // } // } enum Page { - username, - password, - twofactor, - yubikey + username, + password, + twofactor, + yubikey, } interface Token { - token: string; - expires: string; + token: string; + expires: string; } -async function apiRequest(endpoint: string, method: "GET" | "POST" | "DELETE" | "PUT" = "GET", body: string = undefined) { - return fetch(endpoint, { - method, - body, - credentials: "same-origin", - headers: { - "content-type": "application/json" - } - }).then(e => { - if (e.status !== 200) throw new Error(e.statusText) - return e.json() - }).then(data => { - if (data.error) { - return Promise.reject(new Error(data.error)) - } - return data; - }) +async function apiRequest( + endpoint: string, + method: "GET" | "POST" | "DELETE" | "PUT" = "GET", + body: string = undefined +) { + return fetch(endpoint, { + method, + body, + credentials: "same-origin", + headers: { + "content-type": "application/json", + }, + }) + .then((e) => { + if (e.status !== 200) throw new Error(e.statusText); + return e.json(); + }) + .then((data) => { + if (data.error) { + return Promise.reject(new Error(data.error)); + } + return data; + }); } +class App extends Component< + {}, + { + page: Page; + username: string; + salt: string; + twofactor: TwoFactors[]; + twofactor_id: string; + } +> { + login: Token; + special: Token; + constructor() { + super(); + this.state = { + page: Page.username, + username: getCookie("username"), + salt: undefined, + twofactor: [], + twofactor_id: null, + }; + } -class App extends Component<{}, { page: Page, username: string, salt: string, twofactor: TwoFactors[], twofactor_id: string }> { - login: Token; - special: Token; - constructor() { - super(); - this.state = { page: Page.username, username: getCookie("username"), salt: undefined, twofactor: [], twofactor_id: null } - } + setCookies() { + setCookie( + "login", + this.login.token, + new Date(this.login.expires).toUTCString() + ); + setCookie( + "special", + this.special.token, + new Date(this.special.expires).toUTCString() + ); + } - setCookies() { - setCookie("login", this.login.token, new Date(this.login.expires).toUTCString()); - setCookie("special", this.special.token, new Date(this.special.expires).toUTCString()); - } + finish() { + this.setCookies(); + let d = new Date(); + d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days + setCookie("username", this.state.username, d.toUTCString()); + let url = new URL(window.location.href); + let state = url.searchParams.get("state"); + let red = "/"; - finish() { - this.setCookies(); - let d = new Date() - d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000)); //Keep the username 30 days - setCookie("username", this.state.username, d.toUTCString()); - let url = new URL(window.location.href); - let state = url.searchParams.get("state") - let red = "/" + if (state) { + let base64 = url.searchParams.get("base64"); + if (base64) red = atob(state); + else red = state; + } + window.location.href = red; + } - if (state) { - let base64 = url.searchParams.get("base64") - if (base64) - red = atob(state) - else - red = state - } - window.location.href = red; - } + render() { + let cont; + switch (this.state.page) { + case Page.username: + cont = ( + { + this.setState({ username, salt, page: Page.password }); + localStorage.setItem("username", username); + }} + /> + ); + break; + case Page.password: + cont = ( + { + this.login = login; + this.special = special; + this.setCookies(); - render() { - let cont; - switch (this.state.page) { - case Page.username: - cont = { - this.setState({ username, salt, page: Page.password }) - localStorage.setItem("username", username); - }} /> - break; - case Page.password: - cont = { - this.login = login; - this.special = special; - this.setCookies(); - - if (!twofactor) { + if (!twofactor) { this.finish(); - } else { + } else { this.setState({ twofactor, page: Page.twofactor }); - } - }} /> - break; - case Page.twofactor: - cont = { - if (type === TFATypes.YUBI_KEY) { - let { request } = await apiRequest("/api/user/twofactor/yubikey", "GET"); + } + }} + /> + ); + break; + case Page.twofactor: + cont = ( + { + if (type === TFATypes.YUBI_KEY) { + let { request } = await apiRequest( + "/api/user/twofactor/yubikey", + "GET" + ); console.log(request); - (window as any).u2f.sign(request.appId, [request.challenge], [request], async (response) => { - let res = await apiRequest("/api/user/twofactor/yubikey", "PUT", JSON.stringify({ response })); - if (res.success) { - this.login.expires = res.login_exp; - this.special.expires = res.special_exp; - this.finish(); - } - }) - } - }} /> - break; - // case Page.yubikey: - // cont = { - // this.login = login; - // this.special = special; - // this.finish() - // }} /> - // break; - } - return
    + (window as any).u2f.sign( + request.appId, + [request.challenge], + [request], + async (response) => { + let res = await apiRequest( + "/api/user/twofactor/yubikey", + "PUT", + JSON.stringify({ response }) + ); + if (res.success) { + this.login.expires = res.login_exp; + this.special.expires = res.special_exp; + this.finish(); + } + } + ); + } + }} + /> + ); + break; + // case Page.yubikey: + // cont = { + // this.login = login; + // this.special = special; + // this.finish() + // }} /> + // break; + } + return ( +
    -

    Login

    +

    Login

    -
    - {cont} -
    +
    {cont}
    -

    Powered by {appname}

    +

    Powered by {appname}

    -
    - } +
    + ); + } } -document.addEventListener('DOMContentLoaded', function () { - render(, document.body.querySelector("#content")) -}, false) \ No newline at end of file +document.addEventListener( + "DOMContentLoaded", + function () { + render(, document.body.querySelector("#content")); + }, + false +); diff --git a/views/src/login/u2f-api-polyfill.js b/views/src/login/u2f-api-polyfill.js index 347be04..cb372bd 100644 --- a/views/src/login/u2f-api-polyfill.js +++ b/views/src/login/u2f-api-polyfill.js @@ -12,751 +12,833 @@ /** * @fileoverview The U2F api. */ -'use strict'; +"use strict"; // NECESSARY CHANGE: wrap the whole file in a closure -(function (){ - // NECESSARY CHANGE: detect UA to avoid clobbering other browser's U2F API. - var isChrome = 'chrome' in window && window.navigator.userAgent.indexOf('Edge') < 0; - if ('u2f' in window || !isChrome) { - return; - } +(function () { + // NECESSARY CHANGE: detect UA to avoid clobbering other browser's U2F API. + var isChrome = + "chrome" in window && window.navigator.userAgent.indexOf("Edge") < 0; + if ("u2f" in window || !isChrome) { + return; + } - /** - * Namespace for the U2F api. - * @type {Object} - */ - // NECESSARY CHANGE: define the window.u2f API. - var u2f = window.u2f = {}; + /** + * Namespace for the U2F api. + * @type {Object} + */ + // NECESSARY CHANGE: define the window.u2f API. + var u2f = (window.u2f = {}); - /** - * FIDO U2F Javascript API Version - * @number - */ - var js_api_version; + /** + * FIDO U2F Javascript API Version + * @number + */ + var js_api_version; - /** - * The U2F extension id - * @const {string} - */ - // The Chrome packaged app extension ID. - // Uncomment this if you want to deploy a server instance that uses - // the package Chrome app and does not require installing the U2F Chrome extension. - u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; - // The U2F Chrome extension ID. - // Uncomment this if you want to deploy a server instance that uses - // the U2F Chrome extension to authenticate. - // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + /** + * The U2F extension id + * @const {string} + */ + // The Chrome packaged app extension ID. + // Uncomment this if you want to deploy a server instance that uses + // the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = "kmendfapggjehodndflmmgagdbamhnfd"; + // The U2F Chrome extension ID. + // Uncomment this if you want to deploy a server instance that uses + // the U2F Chrome extension to authenticate. + // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + /** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ + u2f.MessageTypes = { + U2F_REGISTER_REQUEST: "u2f_register_request", + U2F_REGISTER_RESPONSE: "u2f_register_response", + U2F_SIGN_REQUEST: "u2f_sign_request", + U2F_SIGN_RESPONSE: "u2f_sign_response", + U2F_GET_API_VERSION_REQUEST: "u2f_get_api_version_request", + U2F_GET_API_VERSION_RESPONSE: "u2f_get_api_version_response", + }; - /** - * Message types for messsages to/from the extension - * @const - * @enum {string} - */ - u2f.MessageTypes = { - 'U2F_REGISTER_REQUEST': 'u2f_register_request', - 'U2F_REGISTER_RESPONSE': 'u2f_register_response', - 'U2F_SIGN_REQUEST': 'u2f_sign_request', - 'U2F_SIGN_RESPONSE': 'u2f_sign_response', - 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', - 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' - }; + /** + * Response status codes + * @const + * @enum {number} + */ + u2f.ErrorCodes = { + OK: 0, + OTHER_ERROR: 1, + BAD_REQUEST: 2, + CONFIGURATION_UNSUPPORTED: 3, + DEVICE_INELIGIBLE: 4, + TIMEOUT: 5, + }; + /** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ + u2f.U2fRequest; - /** - * Response status codes - * @const - * @enum {number} - */ - u2f.ErrorCodes = { - 'OK': 0, - 'OTHER_ERROR': 1, - 'BAD_REQUEST': 2, - 'CONFIGURATION_UNSUPPORTED': 3, - 'DEVICE_INELIGIBLE': 4, - 'TIMEOUT': 5 - }; + /** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ + u2f.U2fResponse; + /** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ + u2f.Error; - /** - * A message for registration requests - * @typedef {{ - * type: u2f.MessageTypes, - * appId: ?string, - * timeoutSeconds: ?number, - * requestId: ?number - * }} - */ - u2f.U2fRequest; + /** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC, USB_INTERNAL}} + */ + u2f.Transport; + /** + * Data object for a single sign request. + * @typedef {Array} + */ + u2f.Transports; - /** - * A message for registration responses - * @typedef {{ - * type: u2f.MessageTypes, - * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), - * requestId: ?number - * }} - */ - u2f.U2fResponse; + /** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ + u2f.SignRequest; + /** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ + u2f.SignResponse; - /** - * An error object for responses - * @typedef {{ - * errorCode: u2f.ErrorCodes, - * errorMessage: ?string - * }} - */ - u2f.Error; + /** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ + u2f.RegisterRequest; - /** - * Data object for a single sign request. - * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC, USB_INTERNAL}} - */ - u2f.Transport; + /** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ + u2f.RegisterResponse; + /** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ + u2f.RegisteredKey; - /** - * Data object for a single sign request. - * @typedef {Array} - */ - u2f.Transports; + /** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ + u2f.GetJsApiVersionResponse; - /** - * Data object for a single sign request. - * @typedef {{ - * version: string, - * challenge: string, - * keyHandle: string, - * appId: string - * }} - */ - u2f.SignRequest; + //Low level MessagePort API support - - /** - * Data object for a sign response. - * @typedef {{ - * keyHandle: string, - * signatureData: string, - * clientData: string - * }} - */ - u2f.SignResponse; - - - /** - * Data object for a registration request. - * @typedef {{ - * version: string, - * challenge: string - * }} - */ - u2f.RegisterRequest; - - - /** - * Data object for a registration response. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: Transports, - * appId: string - * }} - */ - u2f.RegisterResponse; - - - /** - * Data object for a registered key. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: ?Transports, - * appId: ?string - * }} - */ - u2f.RegisteredKey; - - - /** - * Data object for a get API register response. - * @typedef {{ - * js_api_version: number - * }} - */ - u2f.GetJsApiVersionResponse; - - - //Low level MessagePort API support - - /** - * Sets up a MessagePort to the U2F extension using the - * available mechanisms. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - */ - u2f.getMessagePort = function(callback) { - if (typeof chrome != 'undefined' && chrome.runtime) { - // The actual message here does not matter, but we need to get a reply - // for the callback to run. Thus, send an empty signature request - // in order to get a failure response. - var msg = { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: [] - }; - chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { - if (!chrome.runtime.lastError) { - // We are on a whitelisted origin and can talk directly - // with the extension. - u2f.getChromeRuntimePort_(callback); - } else { - // chrome.runtime was available, but we couldn't message - // the extension directly, use iframe - u2f.getIframePort_(callback); - } - }); - } else if (u2f.isAndroidChrome_()) { - u2f.getAuthenticatorPort_(callback); - } else if (u2f.isIosChrome_()) { - u2f.getIosPort_(callback); - } else { - // chrome.runtime was not available at all, which is normal - // when this origin doesn't have access to any extensions. - u2f.getIframePort_(callback); - } - }; - - /** - * Detect chrome running on android based on the browser's useragent. - * @private - */ - u2f.isAndroidChrome_ = function() { - var userAgent = navigator.userAgent; - return userAgent.indexOf('Chrome') != -1 && - userAgent.indexOf('Android') != -1; - }; - - /** - * Detect chrome running on iOS based on the browser's platform. - * @private - */ - u2f.isIosChrome_ = function() { - return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; - }; - - /** - * Connects directly to the extension via chrome.runtime.connect. - * @param {function(u2f.WrappedChromeRuntimePort_)} callback - * @private - */ - u2f.getChromeRuntimePort_ = function(callback) { - var port = chrome.runtime.connect(u2f.EXTENSION_ID, - {'includeTlsChannelId': true}); - setTimeout(function() { - callback(new u2f.WrappedChromeRuntimePort_(port)); - }, 0); - }; - - /** - * Return a 'port' abstraction to the Authenticator app. - * @param {function(u2f.WrappedAuthenticatorPort_)} callback - * @private - */ - u2f.getAuthenticatorPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedAuthenticatorPort_()); - }, 0); - }; - - /** - * Return a 'port' abstraction to the iOS client app. - * @param {function(u2f.WrappedIosPort_)} callback - * @private - */ - u2f.getIosPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedIosPort_()); - }, 0); - }; - - /** - * A wrapper for chrome.runtime.Port that is compatible with MessagePort. - * @param {Port} port - * @constructor - * @private - */ - u2f.WrappedChromeRuntimePort_ = function(port) { - this.port_ = port; - }; - - /** - * Format and return a sign request compliant with the JS API version supported by the extension. - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ - u2f.formatSignRequest_ = - function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: challenge, - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: signRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - appId: appId, - challenge: challenge, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - }; - - /** - * Format and return a register request compliant with the JS API version supported by the extension.. - * @param {Array} signRequests - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ - u2f.formatRegisterRequest_ = - function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - for (var i = 0; i < registerRequests.length; i++) { - registerRequests[i].appId = appId; - } - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: registerRequests[0], - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - signRequests: signRequests, - registerRequests: registerRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - appId: appId, - registerRequests: registerRequests, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - }; - - - /** - * Posts a message on the underlying channel. - * @param {Object} message - */ - u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { - this.port_.postMessage(message); - }; - - - /** - * Emulates the HTML 5 addEventListener interface. Works only for the - * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. - * @param {string} eventName - * @param {function({data: Object})} handler - */ - u2f.WrappedChromeRuntimePort_.prototype.addEventListener = - function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message' || name == 'onmessage') { - this.port_.onMessage.addListener(function(message) { - // Emulate a minimal MessageEvent object - handler({'data': message}); - }); - } else { - console.error('WrappedChromeRuntimePort only supports onMessage'); - } - }; - - /** - * Wrap the Authenticator app with a MessagePort interface. - * @constructor - * @private - */ - u2f.WrappedAuthenticatorPort_ = function() { - this.requestId_ = -1; - this.requestObject_ = null; - } - - /** - * Launch the Authenticator intent. - * @param {Object} message - */ - u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ';S.request=' + encodeURIComponent(JSON.stringify(message)) + - ';end'; - document.location = intentUrl; - }; - - /** - * Tells what type of port this is. - * @return {String} port type - */ - u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { - return "WrappedAuthenticatorPort_"; - }; - - - /** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ - u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message') { - var self = this; - /* Register a callback to that executes when - * chrome injects the response. */ - window.addEventListener( - 'message', self.onRequestUpdate_.bind(self, handler), false); - } else { - console.error('WrappedAuthenticatorPort only supports message'); - } - }; - - /** - * Callback invoked when a response is received from the Authenticator. - * @param function({data: Object}) callback - * @param {Object} message message Object - */ - u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = - function(callback, message) { - var messageObject = JSON.parse(message.data); - var intentUrl = messageObject['intentURL']; - - var errorCode = messageObject['errorCode']; - var responseObject = null; - if (messageObject.hasOwnProperty('data')) { - responseObject = /** @type {Object} */ ( - JSON.parse(messageObject['data'])); - } - - callback({'data': responseObject}); - }; - - /** - * Base URL for intents to Authenticator. - * @const - * @private - */ - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = - 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; - - /** - * Wrap the iOS client app with a MessagePort interface. - * @constructor - * @private - */ - u2f.WrappedIosPort_ = function() {}; - - /** - * Launch the iOS client app request - * @param {Object} message - */ - u2f.WrappedIosPort_.prototype.postMessage = function(message) { - var str = JSON.stringify(message); - var url = "u2f://auth?" + encodeURI(str); - location.replace(url); - }; - - /** - * Tells what type of port this is. - * @return {String} port type - */ - u2f.WrappedIosPort_.prototype.getPortType = function() { - return "WrappedIosPort_"; - }; - - /** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ - u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name !== 'message') { - console.error('WrappedIosPort only supports message'); - } - }; - - /** - * Sets up an embedded trampoline iframe, sourced from the extension. - * @param {function(MessagePort)} callback - * @private - */ - u2f.getIframePort_ = function(callback) { - // Create the iframe - var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; - var iframe = document.createElement('iframe'); - iframe.src = iframeOrigin + '/u2f-comms.html'; - iframe.setAttribute('style', 'display:none'); - document.body.appendChild(iframe); - - var channel = new MessageChannel(); - var ready = function(message) { - if (message.data == 'ready') { - channel.port1.removeEventListener('message', ready); - callback(channel.port1); + /** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ + u2f.getMessagePort = function (callback) { + if (typeof chrome != "undefined" && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [], + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); } else { - console.error('First event on iframe port was not "ready"'); + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); } - }; - channel.port1.addEventListener('message', ready); - channel.port1.start(); + }; - iframe.addEventListener('load', function() { - // Deliver the port to the iframe and initialize - iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); - }); - }; + /** + * Detect chrome running on android based on the browser's useragent. + * @private + */ + u2f.isAndroidChrome_ = function () { + var userAgent = navigator.userAgent; + return ( + userAgent.indexOf("Chrome") != -1 && userAgent.indexOf("Android") != -1 + ); + }; + /** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ + u2f.isIosChrome_ = function () { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; + }; - //High-level JS API + /** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ + u2f.getChromeRuntimePort_ = function (callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, { + includeTlsChannelId: true, + }); + setTimeout(function () { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); + }; - /** - * Default extension response timeout in seconds. - * @const - */ - u2f.EXTENSION_TIMEOUT_SEC = 30; + /** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ + u2f.getAuthenticatorPort_ = function (callback) { + setTimeout(function () { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); + }; - /** - * A singleton instance for a MessagePort to the extension. - * @type {MessagePort|u2f.WrappedChromeRuntimePort_} - * @private - */ - u2f.port_ = null; + /** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ + u2f.getIosPort_ = function (callback) { + setTimeout(function () { + callback(new u2f.WrappedIosPort_()); + }, 0); + }; - /** - * Callbacks waiting for a port - * @type {Array} - * @private - */ - u2f.waitingForPort_ = []; + /** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ + u2f.WrappedChromeRuntimePort_ = function (port) { + this.port_ = port; + }; - /** - * A counter for requestIds. - * @type {number} - * @private - */ - u2f.reqCounter_ = 0; - - /** - * A map from requestIds to client callbacks - * @type {Object.} - * @private - */ - u2f.callbackMap_ = {}; - - /** - * Creates or retrieves the MessagePort singleton to use. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - * @private - */ - u2f.getPortSingleton_ = function(callback) { - if (u2f.port_) { - callback(u2f.port_); - } else { - if (u2f.waitingForPort_.length == 0) { - u2f.getMessagePort(function(port) { - u2f.port_ = port; - u2f.port_.addEventListener('message', - /** @type {function(Event)} */ (u2f.responseHandler_)); - - // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) - u2f.waitingForPort_.shift()(u2f.port_); - }); + /** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ + u2f.formatSignRequest_ = function ( + appId, + challenge, + registeredKeys, + timeoutSeconds, + reqId + ) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId, + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId, + }; } - u2f.waitingForPort_.push(callback); - } - }; - - /** - * Handles response messages from the extension. - * @param {MessageEvent.} message - * @private - */ - u2f.responseHandler_ = function(message) { - var response = message.data; - var reqId = response['requestId']; - if (!reqId || !u2f.callbackMap_[reqId]) { - console.error('Unknown or missing requestId in response.'); - return; - } - var cb = u2f.callbackMap_[reqId]; - delete u2f.callbackMap_[reqId]; - cb(response['responseData']); - }; - - /** - * Dispatches an array of sign requests to available U2F tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the sign request. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ - u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual sign request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual sign request in the supported API version. - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - } - }; - - /** - * Dispatches an array of sign requests to available U2F tokens. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ - u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); - port.postMessage(req); - }); - }; - - /** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the register request. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ - u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual register request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual register request in the supported API version. - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - } - }; - - /** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ - u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatRegisterRequest_( - appId, registeredKeys, registerRequests, timeoutSeconds, reqId); - port.postMessage(req); - }); - }; - - - /** - * Dispatches a message to the extension to find out the supported - * JS API version. - * If the user is on a mobile phone and is thus using Google Authenticator instead - * of the Chrome extension, don't send the request and simply return 0. - * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback - * @param {number=} opt_timeoutSeconds - */ - u2f.getApiVersion = function(callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - // If we are using Android Google Authenticator or iOS client app, - // do not fire an intent to ask which JS API version to use. - if (port.getPortType) { - var apiVersion; - switch (port.getPortType()) { - case 'WrappedIosPort_': - case 'WrappedAuthenticatorPort_': - apiVersion = 1.1; - break; - - default: - apiVersion = 0; - break; - } - callback({ 'js_api_version': apiVersion }); - return; - } - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var req = { - type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, - timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), - requestId: reqId + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId, }; - port.postMessage(req); - }); - }; + }; + + /** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ + u2f.formatRegisterRequest_ = function ( + appId, + registeredKeys, + registerRequests, + timeoutSeconds, + reqId + ) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId, + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId, + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId, + }; + }; + + /** + * Posts a message on the underlying channel. + * @param {Object} message + */ + u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) { + this.port_.postMessage(message); + }; + + /** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ + u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function ( + eventName, + handler + ) { + var name = eventName.toLowerCase(); + if (name == "message" || name == "onmessage") { + this.port_.onMessage.addListener(function (message) { + // Emulate a minimal MessageEvent object + handler({ data: message }); + }); + } else { + console.error("WrappedChromeRuntimePort only supports onMessage"); + } + }; + + /** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ + u2f.WrappedAuthenticatorPort_ = function () { + this.requestId_ = -1; + this.requestObject_ = null; + }; + + /** + * Launch the Authenticator intent. + * @param {Object} message + */ + u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ";S.request=" + + encodeURIComponent(JSON.stringify(message)) + + ";end"; + document.location = intentUrl; + }; + + /** + * Tells what type of port this is. + * @return {String} port type + */ + u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () { + return "WrappedAuthenticatorPort_"; + }; + + /** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ + u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function ( + eventName, + handler + ) { + var name = eventName.toLowerCase(); + if (name == "message") { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + "message", + self.onRequestUpdate_.bind(self, handler), + false + ); + } else { + console.error("WrappedAuthenticatorPort only supports message"); + } + }; + + /** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ + u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function ( + callback, + message + ) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject["intentURL"]; + + var errorCode = messageObject["errorCode"]; + var responseObject = null; + if (messageObject.hasOwnProperty("data")) { + responseObject = /** @type {Object} */ (JSON.parse( + messageObject["data"] + )); + } + + callback({ data: responseObject }); + }; + + /** + * Base URL for intents to Authenticator. + * @const + * @private + */ + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + "intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE"; + + /** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ + u2f.WrappedIosPort_ = function () {}; + + /** + * Launch the iOS client app request + * @param {Object} message + */ + u2f.WrappedIosPort_.prototype.postMessage = function (message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); + }; + + /** + * Tells what type of port this is. + * @return {String} port type + */ + u2f.WrappedIosPort_.prototype.getPortType = function () { + return "WrappedIosPort_"; + }; + + /** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ + u2f.WrappedIosPort_.prototype.addEventListener = function ( + eventName, + handler + ) { + var name = eventName.toLowerCase(); + if (name !== "message") { + console.error("WrappedIosPort only supports message"); + } + }; + + /** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ + u2f.getIframePort_ = function (callback) { + // Create the iframe + var iframeOrigin = "chrome-extension://" + u2f.EXTENSION_ID; + var iframe = document.createElement("iframe"); + iframe.src = iframeOrigin + "/u2f-comms.html"; + iframe.setAttribute("style", "display:none"); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function (message) { + if (message.data == "ready") { + channel.port1.removeEventListener("message", ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener("message", ready); + channel.port1.start(); + + iframe.addEventListener("load", function () { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage("init", iframeOrigin, [ + channel.port2, + ]); + }); + }; + + //High-level JS API + + /** + * Default extension response timeout in seconds. + * @const + */ + u2f.EXTENSION_TIMEOUT_SEC = 30; + + /** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ + u2f.port_ = null; + + /** + * Callbacks waiting for a port + * @type {Array} + * @private + */ + u2f.waitingForPort_ = []; + + /** + * A counter for requestIds. + * @type {number} + * @private + */ + u2f.reqCounter_ = 0; + + /** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ + u2f.callbackMap_ = {}; + + /** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ + u2f.getPortSingleton_ = function (callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function (port) { + u2f.port_ = port; + u2f.port_.addEventListener( + "message", + /** @type {function(Event)} */ (u2f.responseHandler_) + ); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } + }; + + /** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ + u2f.responseHandler_ = function (message) { + var response = message.data; + var reqId = response["requestId"]; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error("Unknown or missing requestId in response."); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response["responseData"]); + }; + + /** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.sign = function ( + appId, + challenge, + registeredKeys, + callback, + opt_timeoutSeconds + ) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion(function (response) { + js_api_version = + response["js_api_version"] === undefined + ? 0 + : response["js_api_version"]; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest( + appId, + challenge, + registeredKeys, + callback, + opt_timeoutSeconds + ); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest( + appId, + challenge, + registeredKeys, + callback, + opt_timeoutSeconds + ); + } + }; + + /** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.sendSignRequest = function ( + appId, + challenge, + registeredKeys, + callback, + opt_timeoutSeconds + ) { + u2f.getPortSingleton_(function (port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = + typeof opt_timeoutSeconds !== "undefined" + ? opt_timeoutSeconds + : u2f.EXTENSION_TIMEOUT_SEC; + var req = u2f.formatSignRequest_( + appId, + challenge, + registeredKeys, + timeoutSeconds, + reqId + ); + port.postMessage(req); + }); + }; + + /** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.register = function ( + appId, + registerRequests, + registeredKeys, + callback, + opt_timeoutSeconds + ) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion(function (response) { + js_api_version = + response["js_api_version"] === undefined + ? 0 + : response["js_api_version"]; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest( + appId, + registerRequests, + registeredKeys, + callback, + opt_timeoutSeconds + ); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest( + appId, + registerRequests, + registeredKeys, + callback, + opt_timeoutSeconds + ); + } + }; + + /** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.sendRegisterRequest = function ( + appId, + registerRequests, + registeredKeys, + callback, + opt_timeoutSeconds + ) { + u2f.getPortSingleton_(function (port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = + typeof opt_timeoutSeconds !== "undefined" + ? opt_timeoutSeconds + : u2f.EXTENSION_TIMEOUT_SEC; + var req = u2f.formatRegisterRequest_( + appId, + registeredKeys, + registerRequests, + timeoutSeconds, + reqId + ); + port.postMessage(req); + }); + }; + + /** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.getApiVersion = function (callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function (port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case "WrappedIosPort_": + case "WrappedAuthenticatorPort_": + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ js_api_version: apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: + typeof opt_timeoutSeconds !== "undefined" + ? opt_timeoutSeconds + : u2f.EXTENSION_TIMEOUT_SEC, + requestId: reqId, + }; + port.postMessage(req); + }); + }; })(); diff --git a/views/src/main/main.js b/views/src/main/main.js index b8c57fe..accefce 100644 --- a/views/src/main/main.js +++ b/views/src/main/main.js @@ -1 +1 @@ -console.log("Hello World") \ No newline at end of file +console.log("Hello World"); diff --git a/views/src/register/register.js b/views/src/register/register.js index ae2d205..83843f8 100644 --- a/views/src/register/register.js +++ b/views/src/register/register.js @@ -1,23 +1,25 @@ import "inputs"; import sha from "sha512"; -import fireEvent from "event" +import fireEvent from "event"; (() => { - const translations = JSON.parse(document.getElementById("error_codes").innerText) + const translations = JSON.parse( + document.getElementById("error_codes").innerText + ); - const regcode = document.getElementById("regcode") - regcode.value = new URL(window.location.href).searchParams.get("regcode") + const regcode = document.getElementById("regcode"); + regcode.value = new URL(window.location.href).searchParams.get("regcode"); fireEvent(regcode, "change"); function showError(element, message) { if (typeof element === "string") - element = document.getElementById(element) + element = document.getElementById(element); - if (!element) console.error("Element not found,", element) + if (!element) console.error("Element not found,", element); element.innerText = message; if (!message) { if (!element.classList.contains("invisible")) - element.classList.add("invisible") + element.classList.add("invisible"); } else { element.classList.remove("invisible"); } @@ -25,7 +27,8 @@ import fireEvent from "event" function makeid(length) { var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < length; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); @@ -33,61 +36,61 @@ import fireEvent from "event" return text; } - const username = document.getElementById("username") - const name = document.getElementById("name") - const mail = document.getElementById("mail") - const password = document.getElementById("password") - const passwordrep = document.getElementById("passwordrep") + const username = document.getElementById("username"); + const name = document.getElementById("name"); + const mail = document.getElementById("mail"); + const password = document.getElementById("password"); + const passwordrep = document.getElementById("passwordrep"); - const radio_male = document.getElementById("radio-male") - const radio_female = document.getElementById("radio-female") - const radio_other = document.getElementById("radio-other") + const radio_male = document.getElementById("radio-male"); + const radio_female = document.getElementById("radio-female"); + const radio_other = document.getElementById("radio-other"); - const registerButton = document.getElementById("registerbutton") + const registerButton = document.getElementById("registerbutton"); registerButton.onclick = () => { - console.log("Register") + console.log("Register"); showError("error"); let error = false; if (!regcode.value) { - showError("err_regcode", translations["noregcode"]) + showError("err_regcode", translations["noregcode"]); error = true; } else { - showError("err_regcode") + showError("err_regcode"); } if (!username.value) { - showError("err_username", translations["nousername"]) + showError("err_username", translations["nousername"]); error = true; } else { - showError("err_username") + showError("err_username"); } if (!name.value) { - showError("err_name", translations["noname"]) + showError("err_name", translations["noname"]); error = true; } else { - showError("err_name") + showError("err_name"); } if (!mail.value) { - showError("err_mail", translations["nomail"]) + showError("err_mail", translations["nomail"]); error = true; } else { - showError("err_mail") + showError("err_mail"); } if (!password.value) { - showError("err_password", translations["nopassword"]) + showError("err_password", translations["nopassword"]); error = true; } else { - showError("err_password") + showError("err_password"); } if (password.value !== passwordrep.value) { - showError("err_passwordrep", translations["nomatch"]) + showError("err_passwordrep", translations["nomatch"]); error = true; } else { - showError("err_passwordrep") + showError("err_passwordrep"); } if (error) return; @@ -95,14 +98,14 @@ import fireEvent from "event" let gender; if (radio_male.checked) { - gender = "male" + gender = "male"; } else if (radio_female.checked) { - gender = "female" + gender = "female"; } else { - gender = "other" + gender = "other"; } - let salt = makeid(10) + let salt = makeid(10); //username, password, salt, mail, gender, name, birthday, regcode @@ -113,37 +116,44 @@ import fireEvent from "event" name: name.value, regcode: regcode.value, salt: salt, - password: sha(salt + password.value) - } + password: sha(salt + password.value), + }; fetch("/api/user/register", { method: "POST", body: JSON.stringify(body), headers: { - 'content-type': 'application/json' + "content-type": "application/json", }, - }).then(async e => { - if (e.status !== 200) return Promise.reject(new Error(await e.text() || e.statusText)); - return e.json() - }).then(data => { - if (data.error) { - if (!Array.isArray(data.error)) return Promise.reject(new Error(data.error)); - let ce = []; - data.error.forEach(e => { - let ef = document.getElementById("err_" + e.field); - if (!ef) ce.push(e); - else { - showError(ef, e.message); - } - }) - if (ce.length > 0) { - showError("error", ce.join("
    ")); - } - } else { - window.location.href = "/login"; - } - }).catch(e => { - showError("error", e.message); }) - } -})() \ No newline at end of file + .then(async (e) => { + if (e.status !== 200) + return Promise.reject( + new Error((await e.text()) || e.statusText) + ); + return e.json(); + }) + .then((data) => { + if (data.error) { + if (!Array.isArray(data.error)) + return Promise.reject(new Error(data.error)); + let ce = []; + data.error.forEach((e) => { + let ef = document.getElementById("err_" + e.field); + if (!ef) ce.push(e); + else { + showError(ef, e.message); + } + }); + if (ce.length > 0) { + showError("error", ce.join("
    ")); + } + } else { + window.location.href = "/login"; + } + }) + .catch((e) => { + showError("error", e.message); + }); + }; +})(); diff --git a/views/src/register/register.scss b/views/src/register/register.scss index c725ab9..6a7692b 100644 --- a/views/src/register/register.scss +++ b/views/src/register/register.scss @@ -36,13 +36,14 @@ form { padding: 3em 2em 2em 2em; background: #fafafa; border: 1px solid #ebebeb; - box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px; + box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, + rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px; } #registerbutton { width: 100%; background: $primary; - text-shadow: 1px 1px 0 rgba(39, 110, 204, .5); + text-shadow: 1px 1px 0 rgba(39, 110, 204, 0.5); } footer { @@ -52,13 +53,13 @@ footer { footer p { color: #888; font-size: 13px; - letter-spacing: .4px; + letter-spacing: 0.4px; } footer a { color: $primary; text-decoration: none; - transition: all .2s ease; + transition: all 0.2s ease; } footer a:hover { @@ -68,11 +69,11 @@ footer a:hover { footer img { width: 80px; - transition: all .2s ease; + transition: all 0.2s ease; } footer img:hover { - opacity: .83; + opacity: 0.83; } footer img:focus, @@ -92,4 +93,4 @@ footer a:focus { color: $error; margin-top: 5px; font-size: 13px; -} \ No newline at end of file +} diff --git a/views/tsconfig.json b/views/tsconfig.json index 0f9d158..6f77c43 100644 --- a/views/tsconfig.json +++ b/views/tsconfig.json @@ -1,18 +1,9 @@ { - "compilerOptions": { - "lib": [ - "dom", - "es2015", - "es6", - "es7", - "es2018", - "esnext" - ], - "jsxFactory": "h", - "jsx": "react", - "module": "esnext" - }, - "include": [ - "./types.d.ts" - ] -} \ No newline at end of file + "compilerOptions": { + "lib": ["dom", "es2015", "es6", "es7", "es2018", "esnext"], + "jsxFactory": "h", + "jsx": "react", + "module": "esnext" + }, + "include": ["./types.d.ts"] +} diff --git a/views/types.d.ts b/views/types.d.ts index 3b38a3c..e40963c 100644 --- a/views/types.d.ts +++ b/views/types.d.ts @@ -1,9 +1,9 @@ declare module "sha512" { - const val: any; - export default val; + const val: any; + export default val; } declare module "cookie" { - export function getCookie(name: string): string | undefined; - export function setCookie(name: string, value: string, exp?: string): void; -} \ No newline at end of file + export function getCookie(name: string): string | undefined; + export function setCookie(name: string, value: string, exp?: string): void; +} diff --git a/views_repo b/views_repo index e4d08dc..4191522 160000 --- a/views_repo +++ b/views_repo @@ -1 +1 @@ -Subproject commit e4d08dcbf93f58ff85f54f7770ae7db490ff14d6 +Subproject commit 4191522b24334f20f7dcda811ca43959b02fef7a