Running prettier
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Fabian Stamm 2020-08-07 16:16:39 +02:00
parent 77fedd2815
commit 51a8609880
87 changed files with 4000 additions and 2812 deletions

6
package-lock.json generated
View File

@ -2361,6 +2361,12 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true "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": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

View File

@ -19,7 +19,8 @@
"watch-views": "cd views && npm run watch", "watch-views": "cd views && npm run watch",
"install-views_repo": "git submodule init && git submodule update && cd views_repo && npm install ", "install-views_repo": "git submodule init && git submodule update && cd views_repo && npm install ",
"build-views_repo": "cd views_repo && npm run build", "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": { "pipelines": {
"install": [ "install": [
@ -47,6 +48,7 @@
"apidoc": "^0.20.0", "apidoc": "^0.20.0",
"concurrently": "^5.1.0", "concurrently": "^5.1.0",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"prettier": "^2.0.5",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"dependencies": { "dependencies": {

View File

@ -5,7 +5,6 @@ import Client from "../../models/client";
import verify, { Types } from "../middlewares/verify"; import verify, { Types } from "../middlewares/verify";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
const ClientRouter: Router = Router(); const ClientRouter: Router = Router();
ClientRouter.route("/") ClientRouter.route("/")
/** /**
@ -26,11 +25,13 @@ ClientRouter.route("/")
* @apiSuccess {String} clients.client_id Client ID used outside of DB * @apiSuccess {String} clients.client_id Client ID used outside of DB
* @apiSuccess {String} clients.client_secret * @apiSuccess {String} clients.client_secret
*/ */
.get(promiseMiddleware(async (req, res) => { .get(
promiseMiddleware(async (req, res) => {
let clients = await Client.find({}); let clients = await Client.find({});
//ToDo check if user is required! //ToDo check if user is required!
res.json(clients); res.json(clients);
})) })
)
/** /**
* @api {get} /admin/client * @api {get} /admin/client
* @apiName AdminAddClients * @apiName AdminAddClients
@ -55,31 +56,37 @@ ClientRouter.route("/")
* @apiSuccess {String} clients.client_id Client ID used outside of DB * @apiSuccess {String} clients.client_id Client ID used outside of DB
* @apiSuccess {String} clients.client_secret * @apiSuccess {String} clients.client_secret
*/ */
.post(verify({ .post(
verify(
{
internal: { internal: {
type: Types.BOOLEAN, type: Types.BOOLEAN,
optional: true optional: true,
}, },
name: { name: {
type: Types.STRING type: Types.STRING,
}, },
redirect_url: { redirect_url: {
type: Types.STRING type: Types.STRING,
}, },
website: { website: {
type: Types.STRING type: Types.STRING,
}, },
logo: { logo: {
type: Types.STRING, type: Types.STRING,
optional: true optional: true,
} },
}, true), promiseMiddleware(async (req, res) => { },
true
),
promiseMiddleware(async (req, res) => {
req.body.client_secret = randomBytes(32).toString("hex"); req.body.client_secret = randomBytes(32).toString("hex");
let client = Client.new(req.body); let client = Client.new(req.body);
client.maintainer = req.user._id; client.maintainer = req.user._id;
await Client.save(client) await Client.save(client);
res.json(client); res.json(client);
})) })
);
ClientRouter.route("/:id") ClientRouter.route("/:id")
/** /**
@ -92,11 +99,13 @@ ClientRouter.route("/:id")
* *
* @apiSuccess {Boolean} success * @apiSuccess {Boolean} success
*/ */
.delete(promiseMiddleware(async (req, res) => { .delete(
promiseMiddleware(async (req, res) => {
let { id } = req.params; let { id } = req.params;
await Client.delete(id); await Client.delete(id);
res.json({ success: true }); res.json({ success: true });
})) })
)
/** /**
* @api {put} /admin/client/:id * @api {put} /admin/client/:id
* @apiParam {String} id Client _id * @apiParam {String} id Client _id
@ -121,37 +130,46 @@ ClientRouter.route("/:id")
* @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 * @apiSuccess {String} client_secret The client secret, that can be used to obtain token
*/ */
.put(verify({ .put(
verify(
{
internal: { internal: {
type: Types.BOOLEAN, type: Types.BOOLEAN,
optional: true optional: true,
}, },
name: { name: {
type: Types.STRING, type: Types.STRING,
optional: true optional: true,
}, },
redirect_url: { redirect_url: {
type: Types.STRING, type: Types.STRING,
optional: true optional: true,
}, },
website: { website: {
type: Types.STRING, type: Types.STRING,
optional: true optional: true,
}, },
logo: { logo: {
type: Types.STRING, type: Types.STRING,
optional: true optional: true,
} },
}, true), promiseMiddleware(async (req, res) => { },
true
),
promiseMiddleware(async (req, res) => {
let { id } = req.query; let { id } = req.query;
let client = await Client.findById(id); let client = await Client.findById(id);
if (!client) throw new RequestError(req.__("Client not found"), HttpStatusCode.BAD_REQUEST); if (!client)
throw new RequestError(
req.__("Client not found"),
HttpStatusCode.BAD_REQUEST
);
for (let key in req.body) { for (let key in req.body) {
client[key] = req.body[key]; client[key] = req.body[key];
} }
await Client.save(client); await Client.save(client);
res.json(client); res.json(client);
})) })
);
export default ClientRouter; export default ClientRouter;

View File

@ -9,12 +9,16 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error";
const AdminRoute: Router = Router(); const AdminRoute: Router = Router();
AdminRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => { 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); if (!req.isAdmin)
else next() throw new RequestError(
"You have no permission to access this API",
HttpStatusCode.FORBIDDEN
);
else next();
}); });
AdminRoute.use("/client", ClientRoute); AdminRoute.use("/client", ClientRoute);
AdminRoute.use("/regcode", RegCodeRoute) AdminRoute.use("/regcode", RegCodeRoute);
AdminRoute.use("/user", UserRoute) AdminRoute.use("/user", UserRoute);
AdminRoute.use("/permission", PermissionRoute); AdminRoute.use("/permission", PermissionRoute);
export default AdminRoute; export default AdminRoute;

View File

@ -56,18 +56,18 @@ PermissionRoute.route("/")
verify( verify(
{ {
client: { client: {
type: Types.STRING type: Types.STRING,
}, },
name: { name: {
type: Types.STRING type: Types.STRING,
}, },
description: { description: {
type: Types.STRING type: Types.STRING,
}, },
type: { type: {
type: Types.ENUM, type: Types.ENUM,
values: ["user", "client"] values: ["user", "client"],
} },
}, },
true true
), ),
@ -83,7 +83,7 @@ PermissionRoute.route("/")
description: req.body.description, description: req.body.description,
name: req.body.name, name: req.body.name,
client: client._id, client: client._id,
grant_type: req.body.type grant_type: req.body.type,
}); });
await Permission.save(permission); await Permission.save(permission);
res.json(permission); res.json(permission);

View File

@ -21,10 +21,12 @@ RegCodeRoute.route("/")
* @apiSuccess {String} permissions.valid Defines if the Regcode is valid * @apiSuccess {String} permissions.valid Defines if the Regcode is valid
* @apiSuccess {String} permissions.validTill Expiration date of RegCode * @apiSuccess {String} permissions.validTill Expiration date of RegCode
*/ */
.get(promiseMiddleware(async (req, res) => { .get(
promiseMiddleware(async (req, res) => {
let regcodes = await RegCode.find({}); let regcodes = await RegCode.find({});
res.json(regcodes); res.json(regcodes);
})) })
)
/** /**
* @api {delete} /admin/regcode * @api {delete} /admin/regcode
* @apiName AdminDeleteRegcode * @apiName AdminDeleteRegcode
@ -36,11 +38,13 @@ RegCodeRoute.route("/")
* *
* @apiSuccess {Boolean} success * @apiSuccess {Boolean} success
*/ */
.delete(promiseMiddleware(async (req, res) => { .delete(
promiseMiddleware(async (req, res) => {
let { id } = req.query; let { id } = req.query;
await RegCode.delete(id); await RegCode.delete(id);
res.json({ success: true }); res.json({ success: true });
})) })
)
/** /**
* @api {post} /admin/regcode * @api {post} /admin/regcode
* @apiName AdminAddRegcode * @apiName AdminAddRegcode
@ -50,14 +54,16 @@ RegCodeRoute.route("/")
* *
* @apiSuccess {String} code The newly created code * @apiSuccess {String} code The newly created code
*/ */
.post(promiseMiddleware(async (req, res) => { .post(
promiseMiddleware(async (req, res) => {
let regcode = RegCode.new({ let regcode = RegCode.new({
token: randomBytes(10).toString("hex"), token: randomBytes(10).toString("hex"),
valid: true, valid: true,
validTill: moment().add("1", "month").toDate() validTill: moment().add("1", "month").toDate(),
}) });
await RegCode.save(regcode); await RegCode.save(regcode);
res.json({ code: regcode.token }); res.json({ code: regcode.token });
})) })
);
export default RegCodeRoute; export default RegCodeRoute;

View File

@ -9,9 +9,9 @@ import LoginToken from "../../models/login_token";
const UserRoute: Router = Router(); const UserRoute: Router = Router();
UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => { UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN) if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN);
else next() else next();
}) });
UserRoute.route("/") UserRoute.route("/")
/** /**
@ -29,11 +29,15 @@ UserRoute.route("/")
* @apiSuccess {Number} user.gender 0 = none, 1 = male, 2 = female, 3 = other * @apiSuccess {Number} user.gender 0 = none, 1 = male, 2 = female, 3 = other
* @apiSuccess {Boolean} user.admin Is admin or not * @apiSuccess {Boolean} user.admin Is admin or not
*/ */
.get(promiseMiddleware(async (req, res) => { .get(
promiseMiddleware(async (req, res) => {
let users = await User.find({}); let users = await User.find({});
users.forEach(e => delete e.password && delete e.salt && delete e.encryption_key); users.forEach(
(e) => delete e.password && delete e.salt && delete e.encryption_key
);
res.json(users); res.json(users);
})) })
)
/** /**
* @api {delete} /admin/user * @api {delete} /admin/user
* @apiName AdminDeleteUser * @apiName AdminDeleteUser
@ -45,21 +49,23 @@ UserRoute.route("/")
* *
* @apiSuccess {Boolean} success * @apiSuccess {Boolean} success
*/ */
.delete(promiseMiddleware(async (req, res) => { .delete(
promiseMiddleware(async (req, res) => {
let { id } = req.query; let { id } = req.query;
let user = await User.findById(id); let user = await User.findById(id);
await Promise.all([ await Promise.all([
user.mails.map(mail => Mail.delete(mail)), user.mails.map((mail) => Mail.delete(mail)),
[ [
RefreshToken.deleteFilter({ user: user._id }), RefreshToken.deleteFilter({ user: user._id }),
LoginToken.deleteFilter({ user: user._id }) LoginToken.deleteFilter({ user: user._id }),
] ],
]) ]);
await User.delete(user); await User.delete(user);
res.json({ success: true }); res.json({ success: true });
})) })
)
/** /**
* @api {put} /admin/user * @api {put} /admin/user
* @apiName AdminChangeUser * @apiName AdminChangeUser
@ -75,11 +81,13 @@ UserRoute.route("/")
* admin -> user * admin -> user
* user -> admin * user -> admin
*/ */
.put(promiseMiddleware(async (req, res) => { .put(
promiseMiddleware(async (req, res) => {
let { id } = req.query; let { id } = req.query;
let user = await User.findById(id); let user = await User.findById(id);
user.admin = !user.admin; user.admin = !user.admin;
await User.save(user); await User.save(user);
res.json({ success: true }) res.json({ success: true });
})) })
);
export default UserRoute; export default UserRoute;

View File

@ -1,6 +1,9 @@
import { Request, Response, Router } from "express" import { Request, Response, Router } from "express";
import Stacker from "../middlewares/stacker"; import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware, GetClientApiAuthMiddleware } from "../middlewares/client"; import {
GetClientAuthMiddleware,
GetClientApiAuthMiddleware,
} from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user"; import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys"; import { createJWT } from "../../keys";
import Client from "../../models/client"; import Client from "../../models/client";
@ -8,7 +11,6 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error";
import config from "../../config"; import config from "../../config";
import Mail from "../../models/mail"; import Mail from "../../models/mail";
const ClientRouter = Router(); const ClientRouter = Router();
/** /**
@ -24,39 +26,59 @@ const ClientRouter = Router();
* *
* @apiPermission user_client Requires ClientID and Authenticated User * @apiPermission user_client Requires ClientID and Authenticated User
*/ */
ClientRouter.get("/user", Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(false, false), async (req: Request, res: Response) => { ClientRouter.get(
"/user",
Stacker(
GetClientAuthMiddleware(false),
GetUserMiddleware(false, false),
async (req: Request, res: Response) => {
let { redirect_uri, state } = req.query; let { redirect_uri, state } = req.query;
if (redirect_uri !== req.client.redirect_url) if (redirect_uri !== req.client.redirect_url)
throw new RequestError("Invalid redirect URI", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"Invalid redirect URI",
HttpStatusCode.BAD_REQUEST
);
let jwt = await createJWT({ let jwt = await createJWT(
{
client: req.client.client_id, client: req.client.client_id,
uid: req.user.uid, uid: req.user.uid,
username: req.user.username, username: req.user.username,
state: state state: state,
}, { },
{
expiresIn: 30, expiresIn: 30,
issuer: config.core.url, issuer: config.core.url,
algorithm: "RS256", algorithm: "RS256",
subject: req.user.uid, subject: req.user.uid,
audience: req.client.client_id audience: req.client.client_id,
}); //after 30 seconds this token is invalid }
res.redirect(redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : "")); ); //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) => { ClientRouter.get(
let mails = await Promise.all(req.user.mails.map(id => Mail.findById(id))); "/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]; let mail = mails.find((e) => e.primary) || mails[0];
res.json({ res.json({
user: { user: {
username: req.user.username, username: req.user.username,
name: req.user.name, name: req.user.name,
email: mail email: mail,
} },
});
}) })
})); );
export default ClientRouter; export default ClientRouter;

View File

@ -2,7 +2,7 @@ import { Request, Response } from "express";
import Stacker from "../middlewares/stacker"; import Stacker from "../middlewares/stacker";
import { import {
ClientAuthMiddleware, ClientAuthMiddleware,
GetClientAuthMiddleware GetClientAuthMiddleware,
} from "../middlewares/client"; } from "../middlewares/client";
import Permission from "../../models/permissions"; import Permission from "../../models/permissions";
import User from "../../models/user"; import User from "../../models/user";
@ -22,19 +22,19 @@ export const GetPermissions = Stacker(
if (user) { if (user) {
const grant = await Grant.findOne({ const grant = await Grant.findOne({
client: req.client._id, client: req.client._id,
user: user user: user,
}); });
permissions = await Promise.all( permissions = await Promise.all(
grant.permissions.map(perm => Permission.findById(perm)) grant.permissions.map((perm) => Permission.findById(perm))
).then(res => ).then((res) =>
res res
.filter(e => e.grant_type === "client") .filter((e) => e.grant_type === "client")
.map(e => { .map((e) => {
return { return {
id: e._id.toHexString(), id: e._id.toHexString(),
name: e.name, name: e.name,
description: e.description description: e.description,
}; };
}) })
); );
@ -43,10 +43,10 @@ export const GetPermissions = Stacker(
if (permission) { if (permission) {
const grants = await Grant.find({ const grants = await Grant.find({
client: req.client._id, 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 }); res.json({ permissions, users });
@ -73,14 +73,14 @@ export const PostPermissions = Stacker(
let grant = await Grant.findOne({ let grant = await Grant.findOne({
client: req.client._id, client: req.client._id,
user: req.user._id user: req.user._id,
}); });
if (!grant) { if (!grant) {
grant = Grant.new({ grant = Grant.new({
client: req.client._id, client: req.client._id,
user: req.user._id, user: req.user._id,
permissions: [] permissions: [],
}); });
} }
@ -92,7 +92,7 @@ export const PostPermissions = Stacker(
await Grant.save(grant); await Grant.save(grant);
res.json({ res.json({
success: true success: true,
}); });
} }
); );

View File

@ -1,4 +1,4 @@
import * as express from "express" import * as express from "express";
import AdminRoute from "./admin"; import AdminRoute from "./admin";
import UserRoute from "./user"; import UserRoute from "./user";
import InternalRoute from "./internal"; import InternalRoute from "./internal";
@ -9,7 +9,7 @@ import OAuthRoute from "./oauth";
const ApiRouter: express.IRouter = express.Router(); const ApiRouter: express.IRouter = express.Router();
ApiRouter.use("/admin", AdminRoute); ApiRouter.use("/admin", AdminRoute);
ApiRouter.use(cors()) ApiRouter.use(cors());
ApiRouter.use("/user", UserRoute); ApiRouter.use("/user", UserRoute);
ApiRouter.use("/internal", InternalRoute); ApiRouter.use("/internal", InternalRoute);
ApiRouter.use("/oauth", OAuthRoute); ApiRouter.use("/oauth", OAuthRoute);

View File

@ -26,5 +26,5 @@ InternalRoute.get("/oauth", OAuthInternalApp);
* @apiParam {String} uid User ID (either username or UID) * @apiParam {String} uid User ID (either username or UID)
* @apiParam {String} password Hashed and Salted according to specification * @apiParam {String} password Hashed and Salted according to specification
*/ */
InternalRoute.post("/password", PasswordAuth) InternalRoute.post("/password", PasswordAuth);
export default InternalRoute; export default InternalRoute;

View File

@ -6,11 +6,16 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error";
import ClientCode from "../../models/client_code"; import ClientCode from "../../models/client_code";
import moment = require("moment"); import moment = require("moment");
import { randomBytes } from "crypto"; 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) => { async (req: Request, res: Response) => {
let { redirect_uri, state } = req.query let { redirect_uri, state } = req.query;
if (!redirect_uri) { 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 ? "?" : "&"; let sep = redirect_uri.indexOf("?") < 0 ? "?" : "&";
@ -20,10 +25,17 @@ export const OAuthInternalApp = Stacker(GetClientAuthMiddleware(false, true), Us
client: req.client._id, client: req.client._id,
validTill: moment().add(30, "minutes").toDate(), validTill: moment().add(30, "minutes").toDate(),
code: randomBytes(16).toString("hex"), code: randomBytes(16).toString("hex"),
permissions: [] permissions: [],
}); });
await ClientCode.save(code); 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(); res.end();
}); }
);

View File

@ -4,22 +4,32 @@ import Stacker from "../middlewares/stacker";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user"; import User from "../../models/user";
const PasswordAuth = Stacker(GetClientAuthMiddleware(true, true), async (req: Request, res: Response) => { const PasswordAuth = Stacker(
let { username, password, uid }: { username: string, password: string, uid: string } = req.body; 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 }; let query: any = { password: password };
if (username) { if (username) {
query.username = username.toLowerCase() query.username = username.toLowerCase();
} else if (uid) { } else if (uid) {
query.uid = uid; query.uid = uid;
} else { } else {
throw new RequestError(req.__("No username or uid set"), HttpStatusCode.BAD_REQUEST); throw new RequestError(
req.__("No username or uid set"),
HttpStatusCode.BAD_REQUEST
);
} }
let user = await User.findOne(query); let user = await User.findOne(query);
if (!user) { if (!user) {
res.json({ error: req.__("Password or username wrong") }) res.json({ error: req.__("Password or username wrong") });
} else { } else {
res.json({ success: true, uid: user.uid }); res.json({ success: true, uid: user.uid });
} }
}); }
);
export default PasswordAuth; export default PasswordAuth;

View File

@ -6,7 +6,11 @@ import User from "../../models/user";
import Mail from "../../models/mail"; import Mail from "../../models/mail";
import { OAuthJWT } from "../../helper/jwt"; 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) => { return async (req: Request, res: Response, next: NextFunction) => {
try { try {
let client_id = req.query.client_id || req.body.client_id; 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)) { 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 }; 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) { 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) { 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; req.client = client;
next(); next();
@ -44,7 +58,7 @@ export function GetClientAuthMiddleware(checksecret = true, internal = false, ch
if (next) next(e); if (next) next(e);
else throw e; else throw e;
} }
} };
} }
export const ClientAuthMiddleware = GetClientAuthMiddleware(); export const ClientAuthMiddleware = GetClientAuthMiddleware();
@ -52,10 +66,13 @@ export const ClientAuthMiddleware = GetClientAuthMiddleware();
export function GetClientApiAuthMiddleware(permissions?: string[]) { export function GetClientApiAuthMiddleware(permissions?: string[]) {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { try {
const invalid_err = new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED); const invalid_err = new RequestError(
let token: string = req.query.access_token || req.headers.authorization; req.__("You are not logged in or your login is expired"),
if (!token) HttpStatusCode.UNAUTHORIZED
throw invalid_err; );
let token: string =
req.query.access_token || req.headers.authorization;
if (!token) throw invalid_err;
if (token.toLowerCase().startsWith("bearer ")) if (token.toLowerCase().startsWith("bearer "))
token = token.substring(7); token = token.substring(7);
@ -64,19 +81,21 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) {
try { try {
data = await validateJWT(token); data = await validateJWT(token);
} catch (err) { } catch (err) {
throw invalid_err throw invalid_err;
} }
let user = await User.findOne({ uid: data.user }); let user = await User.findOne({ uid: data.user });
if (!user) if (!user) throw invalid_err;
throw invalid_err;
let client = await Client.findOne({ client_id: data.application }) let client = await Client.findOne({ client_id: data.application });
if (!client) if (!client) throw invalid_err;
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; throw invalid_err;
req.user = user; req.user = user;
@ -86,5 +105,5 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) {
if (next) next(e); if (next) next(e);
else throw e; else throw e;
} }
} };
} }

View File

@ -8,19 +8,21 @@ function call(handler: RH, req: Request, res: Response) {
let p = handler(req, res, (err) => { let p = handler(req, res, (err) => {
if (err) no(err); if (err) no(err);
else yes(); else yes();
}) });
if (p && p.catch) p.catch(err => no(err)); if (p && p.catch) p.catch((err) => no(err));
}) });
} }
const Stacker = (...handler: RH[]) => { const Stacker = (...handler: RH[]) => {
return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => { return promiseMiddleware(
async (req: Request, res: Response, next: NextFunction) => {
let hc = handler.concat(); let hc = handler.concat();
while (hc.length > 0) { while (hc.length > 0) {
let h = hc.shift(); let h = hc.shift();
await call(h, req, res); await call(h, req, res);
} }
next(); next();
}); }
} );
};
export default Stacker; export default Stacker;

View File

@ -22,7 +22,7 @@ export function GetUserMiddleware(
redirect_uri?: string, redirect_uri?: string,
validated = true validated = true
) { ) {
return promiseMiddleware(async function( return promiseMiddleware(async function (
req: Request, req: Request,
res: Response, res: Response,
next?: NextFunction next?: NextFunction
@ -57,7 +57,7 @@ export function GetUserMiddleware(
token: special, token: special,
special: true, special: true,
valid: true, valid: true,
user: token.user user: token.user,
}); });
if (!(await CheckToken(special_token, validated))) if (!(await CheckToken(special_token, validated)))
invalid("Special token invalid"); invalid("Special token invalid");
@ -68,7 +68,7 @@ export function GetUserMiddleware(
req.isAdmin = user.admin; req.isAdmin = user.admin;
req.token = { req.token = {
login: token, login: token,
special: special_token special: special_token,
}; };
if (next) next(); if (next) next();

View File

@ -1,6 +1,14 @@
import { Request, Response, NextFunction } from "express" import { Request, Response, NextFunction } from "express";
import { Logging } from "@hibas123/nodelogging"; 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"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
export enum Types { export enum Types {
@ -11,39 +19,41 @@ export enum Types {
OBJECT, OBJECT,
DATE, DATE,
ARRAY, ARRAY,
ENUM ENUM,
} }
function isEmail(value: any): boolean { 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 { export interface CheckObject {
type: Types type: Types;
query?: boolean query?: boolean;
optional?: boolean optional?: boolean;
/** /**
* Only when Type.ENUM * Only when Type.ENUM
* *
* values to check before * values to check before
*/ */
values?: string[] values?: string[];
/** /**
* Only when Type.STRING * Only when Type.STRING
*/ */
notempty?: boolean // Only STRING notempty?: boolean; // Only STRING
} }
export interface Checks { export interface Checks {
[index: string]: CheckObject// | Types [index: string]: CheckObject; // | Types
} }
// req: Request, res: Response, next: NextFunction // req: Request, res: Response, next: NextFunction
export default function (fields: Checks, noadditional = false) { export default function (fields: Checks, noadditional = false) {
return (req: Request, res: Response, next: NextFunction) => { 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) { function check(data: any, field_name: string, field: CheckObject) {
if (data !== undefined && data !== null) { if (data !== undefined && data !== null) {
@ -78,48 +88,60 @@ export default function (fields: Checks, noadditional = false) {
} }
break; break;
default: 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({ errors.push({
message: res.__("Field {{field}} has wrong type. It should be from type {{type}}", { field: field_name, type: Types[field.type].toLowerCase() }), message: res.__(
field: field_name "Field {{field}} has wrong type. It should be from type {{type}}",
}) { field: field_name, type: Types[field.type].toLowerCase() }
),
field: field_name,
});
} else { } else {
if (!field.optional) errors.push({ if (!field.optional)
message: res.__("Field {{field}} is not defined", { field: field_name }), errors.push({
field: field_name message: res.__("Field {{field}} is not defined", {
}) field: field_name,
}),
field: field_name,
});
} }
} }
for (let field_name in fields) { for (let field_name in fields) {
let field = fields[field_name] let field = fields[field_name];
let data = fields[field_name].query ? req.query[field_name] : req.body[field_name] let data = fields[field_name].query
check(data, field_name, field) ? 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); 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); let has = Object.keys(req.body);
has.every(e => { has.every((e) => {
if (should.indexOf(e) >= 0) { if (should.indexOf(e) >= 0) {
return true; return true;
} else { } else {
errors.push({ errors.push({
message: res.__("Field {{field}} should not be there", { field: e }), message: res.__("Field {{field}} should not be there", {
field: e field: e,
}) }),
field: e,
});
return false; return false;
} }
}) });
} }
if (errors.length > 0) { if (errors.length > 0) {
let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true); let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true);
next(err); next(err);
} else } else next();
next() };
}
} }

View File

@ -93,11 +93,13 @@ const GetAuthRoute = (view = false) =>
redirect_uri, redirect_uri,
scope, scope,
state, state,
nored nored,
} = req.query; } = req.query;
const sendError = type => { const sendError = (type) => {
if (redirect_uri === "$local") redirect_uri = "/code"; 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(";"); const scopes = scope.split(";");
@ -123,7 +125,7 @@ const GetAuthRoute = (view = false) =>
let permissions: IPermission[] = []; let permissions: IPermission[] = [];
let proms: PromiseLike<void>[] = []; let proms: PromiseLike<void>[] = [];
if (scopes) { 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; let oid = undefined;
try { try {
oid = new ObjectID(perm); oid = new ObjectID(perm);
@ -132,7 +134,7 @@ const GetAuthRoute = (view = false) =>
continue; continue;
} }
proms.push( proms.push(
Permission.findById(oid).then(p => { Permission.findById(oid).then((p) => {
if (!p) return Promise.reject(new Error()); if (!p) return Promise.reject(new Error());
permissions.push(p); permissions.push(p);
}) })
@ -141,7 +143,7 @@ const GetAuthRoute = (view = false) =>
} }
let err = undefined; let err = undefined;
await Promise.all(proms).catch(e => { await Promise.all(proms).catch((e) => {
err = e; err = e;
}); });
@ -152,7 +154,7 @@ const GetAuthRoute = (view = false) =>
let grant: IGrant | undefined = await Grant.findOne({ let grant: IGrant | undefined = await Grant.findOne({
client: client._id, client: client._id,
user: req.user._id user: req.user._id,
}); });
Logging.debug("Grant", grant, permissions); Logging.debug("Grant", grant, permissions);
@ -161,14 +163,14 @@ const GetAuthRoute = (view = false) =>
if (grant) { if (grant) {
missing_permissions = grant.permissions missing_permissions = grant.permissions
.map(perm => permissions.find(p => p._id.equals(perm))) .map((perm) => permissions.find((p) => p._id.equals(perm)))
.filter(e => !!e); .filter((e) => !!e);
} else { } else {
missing_permissions = permissions; missing_permissions = permissions;
} }
let client_granted_perm = missing_permissions.filter( let client_granted_perm = missing_permissions.filter(
e => e.grant_type == "client" (e) => e.grant_type == "client"
); );
if (client_granted_perm.length > 0) { if (client_granted_perm.length > 0) {
return sendError("no_permission"); return sendError("no_permission");
@ -186,11 +188,11 @@ const GetAuthRoute = (view = false) =>
GetAuthPage( GetAuthPage(
req.__, req.__,
client.name, client.name,
permissions.map(perm => { permissions.map((perm) => {
return { return {
name: perm.name, name: perm.name,
description: perm.description, description: perm.description,
logo: client.logo logo: client.logo,
}; };
}) })
) )
@ -202,11 +204,11 @@ const GetAuthRoute = (view = false) =>
grant = Grant.new({ grant = Grant.new({
client: client._id, client: client._id,
user: req.user._id, user: req.user._id,
permissions: [] permissions: [],
}); });
grant.permissions.push( grant.permissions.push(
...missing_permissions.map(e => e._id) ...missing_permissions.map((e) => e._id)
); );
await Grant.save(grant); await Grant.save(grant);
} else { } else {
@ -218,20 +220,19 @@ const GetAuthRoute = (view = false) =>
let code = ClientCode.new({ let code = ClientCode.new({
user: req.user._id, user: req.user._id,
client: client._id, client: client._id,
permissions: permissions.map(p => p._id), permissions: permissions.map((p) => p._id),
validTill: moment() validTill: moment().add(30, "minutes").toDate(),
.add(30, "minutes") code: randomBytes(16).toString("hex"),
.toDate(),
code: randomBytes(16).toString("hex")
}); });
await ClientCode.save(code); await ClientCode.save(code);
let redir = let redir =
client.redirect_url === "$local" ? "/code" : client.redirect_url; 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") { if (nored === "true") {
res.json({ res.json({
redirect_uri: ruri redirect_uri: ruri,
}); });
} else { } else {
res.redirect(ruri); res.redirect(ruri);

View File

@ -32,7 +32,7 @@ OAuthRoue.post("/auth", GetAuthRoute(false));
* *
* @apiSuccess {String} token The JWT that allowes the application to access the recources granted for refresh token * @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 * @api {get} /oauth/public
@ -43,7 +43,7 @@ OAuthRoue.get("/jwt", JWTRoute)
* *
* @apiSuccess {String} public_key The applications public_key. Used to verify JWT. * @apiSuccess {String} public_key The applications public_key. Used to verify JWT.
*/ */
OAuthRoue.get("/public", Public) OAuthRoue.get("/public", Public);
/** /**
* @api {get} /oauth/refresh * @api {get} /oauth/refresh

View File

@ -8,21 +8,36 @@ import { getAccessTokenJWT } from "../../helper/jwt";
const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => { const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
let { refreshtoken } = req.query; 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 }); 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); let user = await User.findById(token.user);
if (!user) { if (!user) {
token.valid = false; token.valid = false;
await RefreshToken.save(token); 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 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 }); res.json({ token: jwt });
}) });
export default JWTRoute; export default JWTRoute;

View File

@ -2,5 +2,5 @@ import { Request, Response } from "express";
import { public_key } from "../../keys"; import { public_key } from "../../keys";
export default function Public(req: Request, res: Response) { export default function Public(req: Request, res: Response) {
res.json({ public_key: public_key }) res.json({ public_key: public_key });
} }

View File

@ -2,9 +2,13 @@ import { Request, Response } from "express";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user"; import User from "../../models/user";
import Client from "../../models/client"; 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 Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client" import { GetClientAuthMiddleware } from "../middlewares/client";
import ClientCode from "../../models/client_code"; import ClientCode from "../../models/client_code";
import Mail from "../../models/mail"; import Mail from "../../models/mail";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
@ -25,21 +29,26 @@ token, which will inform the authorization server of the breach.
const refreshTokenValidTime = moment.duration(6, "month"); const refreshTokenValidTime = moment.duration(6, "month");
const RefreshTokenRoute = Stacker(GetClientAuthMiddleware(false, false, true), async (req: Request, res: Response) => { const RefreshTokenRoute = Stacker(
GetClientAuthMiddleware(false, false, true),
async (req: Request, res: Response) => {
let grant_type = req.query.grant_type || req.body.grant_type; let grant_type = req.query.grant_type || req.body.grant_type;
if (!grant_type || grant_type === "authorization_code") { if (!grant_type || grant_type === "authorization_code") {
let code = req.query.code || req.body.code; let code = req.query.code || req.body.code;
let nonce = req.query.nonce || req.body.nonce; let nonce = req.query.nonce || req.body.nonce;
let c = await ClientCode.findOne({ code: code }) let c = await ClientCode.findOne({ code: code });
if (!c || moment(c.validTill).isBefore()) { if (!c || moment(c.validTill).isBefore()) {
throw new RequestError(req.__("Invalid code"), HttpStatusCode.BAD_REQUEST); throw new RequestError(
req.__("Invalid code"),
HttpStatusCode.BAD_REQUEST
);
} }
let client = await Client.findById(c.client); let client = await Client.findById(c.client);
let user = await User.findById(c.user); let user = await User.findById(c.user);
let mails = await Promise.all(user.mails.map(m => Mail.findOne(m))); let mails = await Promise.all(user.mails.map((m) => Mail.findOne(m)));
let token = RefreshToken.new({ let token = RefreshToken.new({
user: c.user, user: c.user,
@ -47,12 +56,12 @@ const RefreshTokenRoute = Stacker(GetClientAuthMiddleware(false, false, true), a
permissions: c.permissions, permissions: c.permissions,
token: randomBytes(16).toString("hex"), token: randomBytes(16).toString("hex"),
valid: true, valid: true,
validTill: moment().add(refreshTokenValidTime).toDate() validTill: moment().add(refreshTokenValidTime).toDate(),
}); });
await RefreshToken.save(token); await RefreshToken.save(token);
await ClientCode.delete(c); await ClientCode.delete(c);
let mail = mails.find(e => e.primary); let mail = mails.find((e) => e.primary);
if (!mail) mail = mails[0]; if (!mail) mail = mails[0];
res.json({ res.json({
@ -61,7 +70,7 @@ const RefreshTokenRoute = Stacker(GetClientAuthMiddleware(false, false, true), a
access_token: await getAccessTokenJWT({ access_token: await getAccessTokenJWT({
client: client, client: client,
user: user, user: user,
permissions: c.permissions permissions: c.permissions,
}), }),
token_type: "bearer", token_type: "bearer",
expires_in: AccessTokenJWTExp.asSeconds(), expires_in: AccessTokenJWTExp.asSeconds(),
@ -69,28 +78,46 @@ const RefreshTokenRoute = Stacker(GetClientAuthMiddleware(false, false, true), a
uid: user.uid, uid: user.uid,
email: mail ? mail.mail : "", email: mail ? mail.mail : "",
name: user.name, name: user.name,
enc_key: getEncryptionKey(user, client) enc_key: getEncryptionKey(user, client),
}, },
id_token: getIDToken(user, client.client_id, nonce) id_token: getIDToken(user, client.client_id, nonce),
}); });
} else if (grant_type === "refresh_token") { } else if (grant_type === "refresh_token") {
let refresh_token = req.query.refresh_token || req.body.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); if (!refresh_token)
throw new RequestError(
req.__("refresh_token not set"),
HttpStatusCode.BAD_REQUEST
);
let token = await RefreshToken.findOne({ token: refresh_token }); let token = await RefreshToken.findOne({ token: refresh_token });
if (!token || !token.valid || moment(token.validTill).isBefore()) if (!token || !token.valid || moment(token.validTill).isBefore())
throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST); throw new RequestError(
req.__("Invalid token"),
HttpStatusCode.BAD_REQUEST
);
token.validTill = moment().add(refreshTokenValidTime).toDate(); token.validTill = moment().add(refreshTokenValidTime).toDate();
await RefreshToken.save(token); await RefreshToken.save(token);
let user = await User.findById(token.user); let user = await User.findById(token.user);
let client = await Client.findById(token.client) let client = await Client.findById(token.client);
let jwt = await getAccessTokenJWT({ user, client, permissions: token.permissions }); let jwt = await getAccessTokenJWT({
res.json({ access_token: jwt, expires_in: AccessTokenJWTExp.asSeconds() }); user,
client,
permissions: token.permissions,
});
res.json({
access_token: jwt,
expires_in: AccessTokenJWTExp.asSeconds(),
});
} else { } else {
throw new RequestError("invalid grant_type", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"invalid grant_type",
HttpStatusCode.BAD_REQUEST
);
} }
}) }
);
export default RefreshTokenRoute; export default RefreshTokenRoute;

View File

@ -4,7 +4,9 @@ import { GetUserMiddleware } from "../middlewares/user";
import LoginToken, { CheckToken } from "../../models/login_token"; import LoginToken, { CheckToken } from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
export const GetAccount = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { export const GetAccount = Stacker(
GetUserMiddleware(true, true),
async (req: Request, res: Response) => {
let user = { let user = {
id: req.user.uid, id: req.user.uid,
name: req.user.name, name: req.user.name,
@ -13,4 +15,5 @@ export const GetAccount = Stacker(GetUserMiddleware(true, true), async (req: Req
gender: req.user.gender, gender: req.user.gender,
}; };
res.json({ user }); res.json({ user });
}); }
);

View File

@ -3,14 +3,17 @@ import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user"; import { GetUserMiddleware } from "../middlewares/user";
import Mail from "../../models/mail"; import Mail from "../../models/mail";
export const GetContactInfos = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { export const GetContactInfos = Stacker(
GetUserMiddleware(true, true),
async (req: Request, res: Response) => {
let mails = await Promise.all( let mails = await Promise.all(
req.user.mails.map(mail => Mail.findById(mail)) req.user.mails.map((mail) => Mail.findById(mail))
); );
let contact = { let contact = {
mails: mails.filter(e => !!e), mails: mails.filter((e) => !!e),
phones: req.user.phones phones: req.user.phones,
}; };
res.json({ contact }); res.json({ contact });
}); }
);

View File

@ -62,7 +62,7 @@ UserRoute.post("/register", Register);
* @apiSuccess {String} tfa.name The name of the TFA Method * @apiSuccess {String} tfa.name The name of the TFA Method
* @apiSuccess {String} tfa.type The type 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); UserRoute.use("/twofactor", TwoFactorRoute);
/** /**
@ -95,7 +95,6 @@ UserRoute.get("/token", GetToken);
*/ */
UserRoute.delete("/token/:id", DeleteToken); UserRoute.delete("/token/:id", DeleteToken);
/** /**
* @api {delete} /user/account * @api {delete} /user/account
* @apiName UserGetAccount * @apiName UserGetAccount

View File

@ -1,4 +1,4 @@
import { Request, Response } from "express" import { Request, Response } from "express";
import User, { IUser } from "../../models/user"; import User, { IUser } from "../../models/user";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import moment = require("moment"); import moment = require("moment");
@ -12,36 +12,39 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
let type = req.query.type; let type = req.query.type;
if (type === "username") { if (type === "username") {
let { username, uid } = req.query; 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) { if (!user) {
res.json({ error: req.__("User not found") }) res.json({ error: req.__("User not found") });
} else { } else {
res.json({ salt: user.salt, uid: user.uid }); res.json({ salt: user.salt, uid: user.uid });
} }
return; return;
} else if (type === "password") { } else if (type === "password") {
const sendToken = async (user: IUser, tfa?: any[]) => { 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 = { let client = {
ip: Array.isArray(ip) ? ip[0] : ip, 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 token_str = randomBytes(16).toString("hex");
let tfa_exp = moment().add(5, "minutes").toDate() let tfa_exp = moment().add(5, "minutes").toDate();
let token_exp = moment().add(6, "months").toDate() let token_exp = moment().add(6, "months").toDate();
let token = LoginToken.new({ let token = LoginToken.new({
token: token_str, token: token_str,
valid: true, valid: true,
validTill: tfa ? tfa_exp : token_exp, validTill: tfa ? tfa_exp : token_exp,
user: user._id, user: user._id,
validated: tfa ? false : true, validated: tfa ? false : true,
...client ...client,
}); });
await LoginToken.save(token); await LoginToken.save(token);
let special_str = randomBytes(24).toString("hex"); 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({ let special = LoginToken.new({
token: special_str, token: special_str,
valid: true, valid: true,
@ -49,50 +52,74 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
special: true, special: true,
user: user._id, user: user._id,
validated: tfa ? false : true, validated: tfa ? false : true,
...client ...client,
}); });
await LoginToken.save(special); await LoginToken.save(special);
res.json({ res.json({
login: { token: token_str, expires: token.validTill.toUTCString() }, login: { token: token_str, expires: token.validTill.toUTCString() },
special: { token: special_str, expires: special.validTill.toUTCString() }, special: {
tfa token: special_str,
expires: special.validTill.toUTCString(),
},
tfa,
}); });
} };
let { username, password, uid, date } = req.body; 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) { if (!user) {
res.json({ error: req.__("User not found") }) res.json({ error: req.__("User not found") });
} else { } else {
let upw = user.password; let upw = user.password;
if (date) { if (date) {
if (!moment(date).isBetween(moment().subtract(1, "minute"), moment().add(1, "minute"))) { if (
res.json({ error: req.__("Invalid timestamp. Please check your devices time!") }); !moment(date).isBetween(
moment().subtract(1, "minute"),
moment().add(1, "minute")
)
) {
res.json({
error: req.__(
"Invalid timestamp. Please check your devices time!"
),
});
return; return;
} else { } else {
upw = crypto.createHash("sha512").update(upw + date.toString()).digest("hex"); upw = crypto
.createHash("sha512")
.update(upw + date.toString())
.digest("hex");
} }
} }
if (upw !== password) { if (upw !== password) {
res.json({ error: req.__("Password or username wrong") }) res.json({ error: req.__("Password or username wrong") });
} else { } else {
let twofactor = await TwoFactor.find({ user: user._id, valid: true }) let twofactor = await TwoFactor.find({
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false) user: user._id,
await Promise.all(expired.map(e => { valid: true,
});
let expired = twofactor.filter((e) =>
e.expires ? moment().isAfter(moment(e.expires)) : false
);
await Promise.all(
expired.map((e) => {
e.valid = false; e.valid = false;
return TwoFactor.save(e); return TwoFactor.save(e);
})); })
twofactor = twofactor.filter(e => e.valid); );
twofactor = twofactor.filter((e) => e.valid);
if (twofactor && twofactor.length > 0) { if (twofactor && twofactor.length > 0) {
let tfa = twofactor.map(e => { let tfa = twofactor.map((e) => {
return { return {
id: e._id, id: e._id,
name: e.name || TFANames.get(e.type), name: e.name || TFANames.get(e.type),
type: e.type type: e.type,
} };
}) });
await sendToken(user, tfa); await sendToken(user, tfa);
} else { } else {
await sendToken(user); await sendToken(user);

View File

@ -1,4 +1,4 @@
import { Request, Response, Router } from "express" import { Request, Response, Router } from "express";
import Stacker from "../middlewares/stacker"; import Stacker from "../middlewares/stacker";
import verify, { Types } from "../middlewares/verify"; import verify, { Types } from "../middlewares/verify";
import promiseMiddleware from "../../helper/promiseMiddleware"; import promiseMiddleware from "../../helper/promiseMiddleware";
@ -7,83 +7,93 @@ import { HttpStatusCode } from "../../helper/request_error";
import Mail from "../../models/mail"; import Mail from "../../models/mail";
import RegCode from "../../models/regcodes"; import RegCode from "../../models/regcodes";
const Register = Stacker(verify({ const Register = Stacker(
verify({
mail: { mail: {
type: Types.EMAIL, type: Types.EMAIL,
notempty: true notempty: true,
}, },
username: { username: {
type: Types.STRING, type: Types.STRING,
notempty: true notempty: true,
}, },
password: { password: {
type: Types.STRING, type: Types.STRING,
notempty: true notempty: true,
}, },
salt: { salt: {
type: Types.STRING, type: Types.STRING,
notempty: true notempty: true,
}, },
regcode: { regcode: {
type: Types.STRING, type: Types.STRING,
notempty: true notempty: true,
}, },
gender: { gender: {
type: Types.STRING, type: Types.STRING,
notempty: true notempty: true,
}, },
name: { name: {
type: Types.STRING, type: Types.STRING,
notempty: true notempty: true,
}, },
// birthday: { // birthday: {
// type: Types.DATE // type: Types.DATE
// } // }
}), promiseMiddleware(async (req: Request, res: Response) => { }),
let { username, password, salt, mail, gender, name, birthday, regcode } = req.body; promiseMiddleware(async (req: Request, res: Response) => {
let u = await User.findOne({ username: username.toLowerCase() }) let {
username,
password,
salt,
mail,
gender,
name,
birthday,
regcode,
} = req.body;
let u = await User.findOne({ username: username.toLowerCase() });
if (u) { if (u) {
let err = { let err = {
message: [ message: [
{ {
message: req.__("Username taken"), message: req.__("Username taken"),
field: "username" field: "username",
} },
], ],
status: HttpStatusCode.BAD_REQUEST, status: HttpStatusCode.BAD_REQUEST,
nolog: true nolog: true,
} };
throw err; throw err;
} }
let m = await Mail.findOne({ mail: mail });
let m = await Mail.findOne({ mail: mail })
if (m) { if (m) {
let err = { let err = {
message: [ message: [
{ {
message: req.__("Mail linked with other account"), message: req.__("Mail linked with other account"),
field: "mail" field: "mail",
} },
], ],
status: HttpStatusCode.BAD_REQUEST, status: HttpStatusCode.BAD_REQUEST,
nolog: true nolog: true,
} };
throw err; throw err;
} }
let regc = await RegCode.findOne({ token: regcode }) let regc = await RegCode.findOne({ token: regcode });
if (!regc) { if (!regc) {
let err = { let err = {
message: [ message: [
{ {
message: req.__("Invalid registration code"), message: req.__("Invalid registration code"),
field: "regcode" field: "regcode",
} },
], ],
status: HttpStatusCode.BAD_REQUEST, status: HttpStatusCode.BAD_REQUEST,
nolog: true nolog: true,
} };
throw err; throw err;
} }
@ -92,28 +102,28 @@ const Register = Stacker(verify({
message: [ message: [
{ {
message: req.__("Registration code already used"), message: req.__("Registration code already used"),
field: "regcode" field: "regcode",
} },
], ],
status: HttpStatusCode.BAD_REQUEST, status: HttpStatusCode.BAD_REQUEST,
nolog: true nolog: true,
} };
throw err; throw err;
} }
let g = -1; let g = -1;
switch (gender) { switch (gender) {
case "male": case "male":
g = Gender.male g = Gender.male;
break; break;
case "female": case "female":
g = Gender.female g = Gender.female;
break; break;
case "other": case "other":
g = Gender.other g = Gender.other;
break; break;
default: default:
g = Gender.none g = Gender.none;
break; break;
} }
@ -124,21 +134,22 @@ const Register = Stacker(verify({
gender: g, gender: g,
name: name, name: name,
// birthday: birthday, // birthday: birthday,
admin: false admin: false,
}) });
regc.valid = false; regc.valid = false;
await RegCode.save(regc); await RegCode.save(regc);
let ml = Mail.new({ let ml = Mail.new({
mail: mail, mail: mail,
primary: true primary: true,
}) });
await Mail.save(ml); await Mail.save(ml);
user.mails.push(ml._id); user.mails.push(ml._id);
await User.save(user) await User.save(user);
res.json({ success: true }); res.json({ success: true });
})) })
);
export default Register; export default Register;

View File

@ -4,26 +4,42 @@ import { GetUserMiddleware } from "../middlewares/user";
import LoginToken, { CheckToken } from "../../models/login_token"; import LoginToken, { CheckToken } from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
export const GetToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { export const GetToken = Stacker(
let raw_token = await LoginToken.find({ user: req.user._id, valid: true }); GetUserMiddleware(true, true),
let token = await Promise.all(raw_token.map(async token => { 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); await CheckToken(token);
return { return {
id: token._id, id: token._id,
special: token.special, special: token.special,
ip: token.ip, ip: token.ip,
browser: token.browser, browser: token.browser,
isthis: token._id.equals(token.special ? req.token.special._id : req.token.login._id) isthis: token._id.equals(
} token.special ? req.token.special._id : req.token.login._id
}).filter(t => t !== undefined)); ),
};
})
.filter((t) => t !== undefined)
);
res.json({ token }); res.json({ token });
}); }
);
export const DeleteToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { export const DeleteToken = Stacker(
GetUserMiddleware(true, true),
async (req: Request, res: Response) => {
let { id } = req.params; let { id } = req.params;
let token = await LoginToken.findById(id); let token = await LoginToken.findById(id);
if (!token || !token.user.equals(req.user._id)) throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST); if (!token || !token.user.equals(req.user._id))
throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST);
token.valid = false; token.valid = false;
await LoginToken.save(token); await LoginToken.save(token);
res.json({ success: true }); res.json({ success: true });
}); }
);

View File

@ -1,7 +1,10 @@
import { Router } from "express" import { Router } from "express";
import Stacker from "../../../middlewares/stacker"; import Stacker from "../../../middlewares/stacker";
import { GetUserMiddleware } from "../../../middlewares/user"; 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 RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment"); import moment = require("moment");
import { upgradeToken } from "../helper"; import { upgradeToken } from "../helper";
@ -15,14 +18,16 @@ function generateCode(length: number) {
let bytes = crypto.randomBytes(length); let bytes = crypto.randomBytes(length);
let nrs = ""; let nrs = "";
bytes.forEach((b, idx) => { 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; if (nr > 9) nr = 9;
nrs += String(nr); nrs += String(nr);
}) });
return nrs; return nrs;
} }
BackupCodeRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { BackupCodeRoute.post(
"/",
Stacker(GetUserMiddleware(true, true), async (req, res) => {
//Generating new //Generating new
let codes = Array(10).map(() => generateCode(8)); let codes = Array(10).map(() => generateCode(8));
console.log(codes); console.log(codes);
@ -31,44 +36,65 @@ BackupCodeRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res
type: TwoFATypes.OTC, type: TwoFATypes.OTC,
valid: true, valid: true,
data: codes, data: codes,
name: "" name: "",
}) });
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
res.json({ res.json({
codes, codes,
id: twofactor._id id: twofactor._id,
});
}) })
})); );
BackupCodeRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { BackupCodeRoute.put(
"/",
Stacker(
GetUserMiddleware(true, false, undefined, false),
async (req, res) => {
let { login, special } = req.token; let { login, special } = req.token;
let { id, code }: { id: string, code: string } = req.body; let { id, code }: { id: string; code: string } = req.body;
let twofactor: IBackupCode = await TwoFactor.findById(id); let twofactor: IBackupCode = await TwoFactor.findById(id);
if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) { if (
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); !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)) { if (twofactor.expires && moment().isAfter(twofactor.expires)) {
twofactor.valid = false; twofactor.valid = false;
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"Invalid Method!",
HttpStatusCode.BAD_REQUEST
);
} }
code = code.replace(/\s/g, ""); code = code.replace(/\s/g, "");
let valid = twofactor.data.find(c => c === code); let valid = twofactor.data.find((c) => c === code);
if (valid) { if (valid) {
twofactor.data = twofactor.data.filter(c => c !== code); twofactor.data = twofactor.data.filter((c) => c !== code);
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
let [login_exp, special_exp] = await Promise.all([ let [login_exp, special_exp] = await Promise.all([
upgradeToken(login), upgradeToken(login),
upgradeToken(special) upgradeToken(special),
]); ]);
res.json({ success: true, login_exp, special_exp }) res.json({ success: true, login_exp, special_exp });
} else { } else {
throw new RequestError("Invalid or already used code!", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"Invalid or already used code!",
HttpStatusCode.BAD_REQUEST
);
} }
})) }
)
);
export default BackupCodeRoute; export default BackupCodeRoute;

View File

@ -6,7 +6,10 @@ export async function upgradeToken(token: ILoginToken) {
token.valid = true; token.valid = true;
token.validated = true; token.validated = true;
//TODO durations from config //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; token.validTill = expires;
await LoginToken.save(token); await LoginToken.save(token);
return expires; return expires;

View File

@ -3,32 +3,41 @@ import YubiKeyRoute from "./yubikey";
import { GetUserMiddleware } from "../../middlewares/user"; import { GetUserMiddleware } from "../../middlewares/user";
import Stacker from "../../middlewares/stacker"; import Stacker from "../../middlewares/stacker";
import TwoFactor from "../../../models/twofactor"; import TwoFactor from "../../../models/twofactor";
import * as moment from "moment" import * as moment from "moment";
import RequestError, { HttpStatusCode } from "../../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import OTCRoute from "./otc"; import OTCRoute from "./otc";
import BackupCodeRoute from "./backup"; import BackupCodeRoute from "./backup";
const TwoFactorRouter = Router(); const TwoFactorRouter = Router();
TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { TwoFactorRouter.get(
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true }) "/",
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false) Stacker(GetUserMiddleware(true, true), async (req, res) => {
await Promise.all(expired.map(e => { 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; e.valid = false;
return TwoFactor.save(e); return TwoFactor.save(e);
})); })
twofactor = twofactor.filter(e => e.valid); );
let tfa = twofactor.map(e => { twofactor = twofactor.filter((e) => e.valid);
let tfa = twofactor.map((e) => {
return { return {
id: e._id, id: e._id,
name: e.name, name: e.name,
type: e.type type: e.type,
} };
}) });
res.json({ methods: tfa }); res.json({ methods: tfa });
})); })
);
TwoFactorRouter.delete("/:id", Stacker(GetUserMiddleware(true, true), async (req, res) => { TwoFactorRouter.delete(
"/:id",
Stacker(GetUserMiddleware(true, true), async (req, res) => {
let { id } = req.params; let { id } = req.params;
let tfa = await TwoFactor.findById(id); let tfa = await TwoFactor.findById(id);
if (!tfa || !tfa.user.equals(req.user._id)) { if (!tfa || !tfa.user.equals(req.user._id)) {
@ -37,7 +46,8 @@ TwoFactorRouter.delete("/:id", Stacker(GetUserMiddleware(true, true), async (req
tfa.valid = false; tfa.valid = false;
await TwoFactor.save(tfa); await TwoFactor.save(tfa);
res.json({ success: true }); res.json({ success: true });
})); })
);
TwoFactorRouter.use("/yubikey", YubiKeyRoute); TwoFactorRouter.use("/yubikey", YubiKeyRoute);
TwoFactorRouter.use("/otc", OTCRoute); TwoFactorRouter.use("/otc", OTCRoute);

View File

@ -1,7 +1,10 @@
import { Router } from "express" import { Router } from "express";
import Stacker from "../../../middlewares/stacker"; import Stacker from "../../../middlewares/stacker";
import { GetUserMiddleware } from "../../../middlewares/user"; 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 RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment"); import moment = require("moment");
import { upgradeToken } from "../helper"; import { upgradeToken } from "../helper";
@ -13,33 +16,43 @@ import config from "../../../../config";
const OTCRoute = Router(); const OTCRoute = Router();
OTCRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { OTCRoute.post(
"/",
Stacker(GetUserMiddleware(true, true), async (req, res) => {
const { type } = req.query; const { type } = req.query;
if (type === "create") { if (type === "create") {
//Generating new //Generating new
let secret = speakeasy.generateSecret({ let secret = speakeasy.generateSecret({
name: config.core.name, name: config.core.name,
issuer: config.core.name issuer: config.core.name,
}); });
let twofactor = TwoFactor.new(<IOTC>{ let twofactor = TwoFactor.new(<IOTC>{
user: req.user._id, user: req.user._id,
type: TwoFATypes.OTC, type: TwoFATypes.OTC,
valid: false, valid: false,
data: secret.base32 data: secret.base32,
}) });
let dataurl = await qrcode.toDataURL(secret.otpauth_url); let dataurl = await qrcode.toDataURL(secret.otpauth_url);
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
res.json({ res.json({
image: dataurl, image: dataurl,
id: twofactor._id id: twofactor._id,
}) });
} else if (type === "validate") { } else if (type === "validate") {
// Checking code and marking as valid // Checking code and marking as valid
const { code, id } = req.body; const { code, id } = req.body;
Logging.debug(req.body, id); Logging.debug(req.body, id);
let twofactor: IOTC = await TwoFactor.findById(id); let twofactor: IOTC = await TwoFactor.findById(id);
const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) }; const err = () => {
if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC || !twofactor.data || twofactor.valid) { 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); Logging.debug("Not found or wrong user", twofactor);
err(); err();
} }
@ -53,8 +66,8 @@ OTCRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
let valid = speakeasy.totp.verify({ let valid = speakeasy.totp.verify({
secret: twofactor.data, secret: twofactor.data,
encoding: "base32", encoding: "base32",
token: code token: code,
}) });
if (valid) { if (valid) {
twofactor.expires = undefined; twofactor.expires = undefined;
@ -67,39 +80,56 @@ OTCRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
} else { } else {
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST); throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
} }
})); })
);
OTCRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { OTCRoute.put(
"/",
Stacker(
GetUserMiddleware(true, false, undefined, false),
async (req, res) => {
let { login, special } = req.token; let { login, special } = req.token;
let { id, code } = req.body; let { id, code } = req.body;
let twofactor: IOTC = await TwoFactor.findById(id); let twofactor: IOTC = await TwoFactor.findById(id);
if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) { if (
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); !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)) { if (twofactor.expires && moment().isAfter(twofactor.expires)) {
twofactor.valid = false; twofactor.valid = false;
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"Invalid Method!",
HttpStatusCode.BAD_REQUEST
);
} }
let valid = speakeasy.totp.verify({ let valid = speakeasy.totp.verify({
secret: twofactor.data, secret: twofactor.data,
encoding: "base32", encoding: "base32",
token: code token: code,
}) });
if (valid) { if (valid) {
let [login_exp, special_exp] = await Promise.all([ let [login_exp, special_exp] = await Promise.all([
upgradeToken(login), upgradeToken(login),
upgradeToken(special) upgradeToken(special),
]); ]);
res.json({ success: true, login_exp, special_exp }) res.json({ success: true, login_exp, special_exp });
} else { } else {
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST); throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
} }
})) }
)
);
export default OTCRoute; export default OTCRoute;

View File

@ -1,9 +1,12 @@
import { Router, Request } from "express" import { Router, Request } from "express";
import Stacker from "../../../middlewares/stacker"; import Stacker from "../../../middlewares/stacker";
import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user"; import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user";
import * as u2f from "u2f"; import * as u2f from "u2f";
import config from "../../../../config"; 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 RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment"); import moment = require("moment");
import LoginToken from "../../../../models/login_token"; import LoginToken from "../../../../models/login_token";
@ -15,7 +18,9 @@ const U2FRoute = Router();
/** /**
* Registerinf a new YubiKey * Registerinf a new YubiKey
*/ */
U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { U2FRoute.post(
"/",
Stacker(GetUserMiddleware(true, true), async (req, res) => {
const { type } = req.query; const { type } = req.query;
if (type === "challenge") { if (type === "challenge") {
const registrationRequest = u2f.request(config.core.url); const registrationRequest = u2f.request(config.core.url);
@ -25,17 +30,29 @@ U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
type: TwoFATypes.U2F, type: TwoFATypes.U2F,
valid: false, valid: false,
data: { data: {
registration: registrationRequest registration: registrationRequest,
} },
}) });
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
res.json({ request: registrationRequest, id: twofactor._id, appid: config.core.url }); res.json({
request: registrationRequest,
id: twofactor._id,
appid: config.core.url,
});
} else { } else {
const { response, id } = req.body; const { response, id } = req.body;
Logging.debug(req.body, id); Logging.debug(req.body, id);
let twofactor: IYubiKey = await TwoFactor.findById(id); let twofactor: IYubiKey = await TwoFactor.findById(id);
const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) }; const err = () => {
if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.U2F || !twofactor.data.registration || twofactor.valid) { 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); Logging.debug("Not found or wrong user", twofactor);
err(); err();
} }
@ -46,45 +63,66 @@ U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
err(); err();
} }
const result = u2f.checkRegistration(twofactor.data.registration, response); const result = u2f.checkRegistration(
twofactor.data.registration,
response
);
if (result.successful) { if (result.successful) {
twofactor.data = { twofactor.data = {
keyHandle: result.keyHandle, keyHandle: result.keyHandle,
publicKey: result.publicKey publicKey: result.publicKey,
} };
twofactor.expires = undefined; twofactor.expires = undefined;
twofactor.valid = true; twofactor.valid = true;
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
res.json({ success: true }); res.json({ success: true });
} else { } else {
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); throw new RequestError(
result.errorMessage,
HttpStatusCode.BAD_REQUEST
);
} }
} }
})); })
);
U2FRoute.get("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { U2FRoute.get(
"/",
Stacker(
GetUserMiddleware(true, false, undefined, false),
async (req, res) => {
let { login, special } = req.token; let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true }) let twofactor: IYubiKey = await TwoFactor.findOne({
user: req.user._id,
type: TwoFATypes.U2F,
valid: true,
});
if (!twofactor) { if (!twofactor) {
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"Invalid Method!",
HttpStatusCode.BAD_REQUEST
);
} }
if (twofactor.expires) { if (twofactor.expires) {
if (moment().isAfter(twofactor.expires)) { if (moment().isAfter(twofactor.expires)) {
twofactor.valid = false; twofactor.valid = false;
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"Invalid Method!",
HttpStatusCode.BAD_REQUEST
);
} }
} }
let request = u2f.request(config.core.url, twofactor.data.keyHandle); let request = u2f.request(config.core.url, twofactor.data.keyHandle);
login.data = { login.data = {
type: "ykr", type: "ykr",
request request,
}; };
let r;; let r;
if (special) { if (special) {
special.data = login.data; special.data = login.data;
r = LoginToken.save(special); r = LoginToken.save(special);
@ -92,43 +130,77 @@ U2FRoute.get("/", Stacker(GetUserMiddleware(true, false, undefined, false), asyn
await Promise.all([r, LoginToken.save(login)]); await Promise.all([r, LoginToken.save(login)]);
res.json({ request }); res.json({ request });
})) }
)
);
U2FRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { U2FRoute.put(
"/",
Stacker(
GetUserMiddleware(true, false, undefined, false),
async (req, res) => {
let { login, special } = req.token; let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true }) let twofactor: IYubiKey = await TwoFactor.findOne({
user: req.user._id,
type: TwoFATypes.U2F,
valid: true,
});
let { response } = req.body; let { response } = req.body;
if (!twofactor || !login.data || login.data.type !== "ykr" || special && (!special.data || special.data.type !== "ykr")) { if (
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); !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)) { if (twofactor.expires && moment().isAfter(twofactor.expires)) {
twofactor.valid = false; twofactor.valid = false;
await TwoFactor.save(twofactor); await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); throw new RequestError(
"Invalid Method!",
HttpStatusCode.BAD_REQUEST
);
} }
let login_exp; let login_exp;
let special_exp; let special_exp;
let result = u2f.checkSignature(login.data.request, response, twofactor.data.publicKey); let result = u2f.checkSignature(
login.data.request,
response,
twofactor.data.publicKey
);
if (result.successful) { if (result.successful) {
if (special) { if (special) {
let result = u2f.checkSignature(special.data.request, response, twofactor.data.publicKey); let result = u2f.checkSignature(
special.data.request,
response,
twofactor.data.publicKey
);
if (result.successful) { if (result.successful) {
special_exp = await upgradeToken(special); special_exp = await upgradeToken(special);
} } else {
else { throw new RequestError(
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); result.errorMessage,
HttpStatusCode.BAD_REQUEST
);
} }
} }
login_exp = await upgradeToken(login); login_exp = await upgradeToken(login);
} else {
throw new RequestError(
result.errorMessage,
HttpStatusCode.BAD_REQUEST
);
} }
else { res.json({ success: true, login_exp, special_exp });
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
} }
res.json({ success: true, login_exp, special_exp }) )
})) );
export default U2FRoute; export default U2FRoute;

View File

@ -7,64 +7,68 @@ import * as ini from "ini";
dotenv.config(); dotenv.config();
export interface DatabaseConfig { export interface DatabaseConfig {
host: string host: string;
database: string database: string;
} }
export interface WebConfig { export interface WebConfig {
port: string port: string;
secure: "true" | "false" | undefined secure: "true" | "false" | undefined;
} }
export interface CoreConfig { export interface CoreConfig {
name: string name: string;
url: string url: string;
dev: boolean dev: boolean;
} }
export interface Config { export interface Config {
core: CoreConfig core: CoreConfig;
database: DatabaseConfig database: DatabaseConfig;
web: WebConfig web: WebConfig;
} }
const config = parse({ const config = (parse(
{
core: { core: {
dev: { dev: {
default: false, default: false,
type: Boolean type: Boolean,
}, },
name: { name: {
type: String, type: String,
default: "Open Auth" default: "Open Auth",
}, },
url: String url: String,
}, },
database: { database: {
database: { database: {
type: String, type: String,
default: "openauth" default: "openauth",
}, },
host: { host: {
type: String, type: String,
default: "localhost" default: "localhost",
} },
}, },
web: { web: {
port: { port: {
type: Number, type: Number,
default: 3004 default: 3004,
}, },
secure: { secure: {
type: Boolean, type: Boolean,
default: false default: false,
} },
} },
}, "config.ini") as any as Config; },
"config.ini"
) as any) as Config;
if (process.env.DEV === "true") if (process.env.DEV === "true") config.core.dev = true;
config.core.dev = true;
if (config.core.dev) 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; export default config;

View File

@ -1,7 +1,7 @@
import SafeMongo from "@hibas123/safe_mongo"; import SafeMongo from "@hibas123/safe_mongo";
import Config from "./config" import Config from "./config";
let dbname = "openauth" let dbname = "openauth";
let host = "localhost" let host = "localhost";
if (Config.database) { if (Config.database) {
if (Config.database.database) dbname = Config.database.database; if (Config.database.database) dbname = Config.database.database;
if (Config.database.host) host = Config.database.host; if (Config.database.host) host = Config.database.host;

2
src/express.d.ts vendored
View File

@ -11,6 +11,6 @@ declare module "express" {
token: { token: {
login: ILoginToken; login: ILoginToken;
special?: ILoginToken; special?: ILoginToken;
} };
} }
} }

View File

@ -9,42 +9,52 @@ export interface OAuthJWT {
user: string; user: string;
username: string; username: string;
permissions: string[]; permissions: string[];
application: string application: string;
} }
const issuer = config.core.url; const issuer = config.core.url;
export const IDTokenJWTExp = moment.duration(30, "m").asSeconds(); export const IDTokenJWTExp = moment.duration(30, "m").asSeconds();
export function getIDToken(user: IUser, client_id: string, nonce: string) { export function getIDToken(user: IUser, client_id: string, nonce: string) {
return createJWT({ return createJWT(
{
user: user.uid, user: user.uid,
name: user.name, name: user.name,
nickname: user.username, nickname: user.username,
username: user.username, username: user.username,
preferred_username: user.username, preferred_username: user.username,
gender: Gender[user.gender], gender: Gender[user.gender],
nonce nonce,
}, { },
{
expiresIn: IDTokenJWTExp, expiresIn: IDTokenJWTExp,
issuer, issuer,
algorithm: "RS256", algorithm: "RS256",
subject: user.uid, subject: user.uid,
audience: client_id audience: client_id,
}) }
);
} }
export const AccessTokenJWTExp = moment.duration(6, "h"); export const AccessTokenJWTExp = moment.duration(6, "h");
export function getAccessTokenJWT(token: { user: IUser, permissions: ObjectID[], client: IClient }) { export function getAccessTokenJWT(token: {
return createJWT(<OAuthJWT>{ user: IUser;
permissions: ObjectID[];
client: IClient;
}) {
return createJWT(
<OAuthJWT>{
user: token.user.uid, user: token.user.uid,
username: token.user.username, username: token.user.username,
permissions: token.permissions.map(p => p.toHexString()), permissions: token.permissions.map((p) => p.toHexString()),
application: token.client.client_id application: token.client.client_id,
}, { },
{
expiresIn: AccessTokenJWTExp.asSeconds(), expiresIn: AccessTokenJWTExp.asSeconds(),
issuer, issuer,
algorithm: "RS256", algorithm: "RS256",
subject: token.user.uid, subject: token.user.uid,
audience: token.client.client_id audience: token.client.client_id,
}) }
);
} }

View File

@ -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<any>) => (req: Request, res: Response, next: NextFunction) => { export default (
Promise.resolve(fn(req, res, next)).catch(next) fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
} ) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};

View File

@ -1,10 +1,8 @@
/** /**
* Hypertext Transfer Protocol (HTTP) response status codes. * Hypertext Transfer Protocol (HTTP) response status codes.
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
*/ */
export enum HttpStatusCode { export enum HttpStatusCode {
/** /**
* The server has received the request headers and the client should proceed to send the request body * 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). * (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 * 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). * 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 { export default class RequestError extends Error {
constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false, public additional: any = undefined) { constructor(
super("") message: any,
public status: HttpStatusCode,
public nolog: boolean = false,
public additional: any = undefined
) {
super("");
this.message = message; this.message = message;
} }
} }

View File

@ -1,14 +1,18 @@
// import * as crypto from "crypto-js" // import * as crypto from "crypto-js"
import { IUser } from "../models/user"; import { IUser } from "../models/user";
import { IClient } from "../models/client"; import { IClient } from "../models/client";
import * as crypto from "crypto" import * as crypto from "crypto";
function sha512(text: string) { function sha512(text: string) {
let hash = crypto.createHash("sha512") let hash = crypto.createHash("sha512");
hash.update(text) hash.update(text);
return hash.digest("base64") return hash.digest("base64");
} }
export function getEncryptionKey(user: IUser, client: IClient) { export function getEncryptionKey(user: IUser, client: IClient) {
return sha512(sha512(user.encryption_key) + sha512(client._id.toHexString()) + sha512(client.client_id)) return sha512(
sha512(user.encryption_key) +
sha512(client._id.toHexString()) +
sha512(client.client_id)
);
} }

View File

@ -13,41 +13,48 @@ import config from "./config";
// } // }
if (!config.web) { if (!config.web) {
Logging.error("No web config set. Terminating.") Logging.error("No web config set. Terminating.");
process.exit(); process.exit();
} }
import * as i18n from "i18n" import * as i18n from "i18n";
i18n.configure({ i18n.configure({
locales: ["en", "de"], locales: ["en", "de"],
directory: "./locales", directory: "./locales",
}) });
import Web from "./web"; import Web from "./web";
import TestData from "./testdata"; import TestData from "./testdata";
import DB from "./database"; import DB from "./database";
Logging.log("Connecting to Database") Logging.log("Connecting to Database");
if (config.core.dev) { 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 () => { DB.connect()
Logging.log("Database connected") .then(async () => {
if (config.core.dev) Logging.log("Database connected");
await TestData() if (config.core.dev) await TestData();
let web = new Web(config.web) let web = new Web(config.web);
web.listen() web.listen();
let already = new Set(); let already = new Set();
function print(path, layer) { function print(path, layer) {
if (layer.route) { if (layer.route) {
layer.route.stack.forEach(print.bind(null, path.concat(split(layer.route.path)))) layer.route.stack.forEach(
} else if (layer.name === 'router' && layer.handle.stack) { print.bind(null, path.concat(split(layer.route.path)))
layer.handle.stack.forEach(print.bind(null, path.concat(split(layer.regexp)))) );
} else if (layer.name === "router" && layer.handle.stack) {
layer.handle.stack.forEach(
print.bind(null, path.concat(split(layer.regexp)))
);
} else if (layer.method) { } else if (layer.method) {
let me: string = layer.method.toUpperCase(); let me: string = layer.method.toUpperCase();
me += " ".repeat(6 - me.length); me += " ".repeat(6 - me.length);
let msg = `${me} /${path.concat(split(layer.regexp)).filter(Boolean).join('/')}`; let msg = `${me} /${path
.concat(split(layer.regexp))
.filter(Boolean)
.join("/")}`;
if (!already.has(msg)) { if (!already.has(msg)) {
already.add(msg); already.add(msg);
Logging.log(msg); Logging.log(msg);
@ -56,24 +63,28 @@ DB.connect().then(async () => {
} }
function split(thing) { function split(thing) {
if (typeof thing === 'string') { if (typeof thing === "string") {
return thing.split('/') return thing.split("/");
} else if (thing.fast_slash) { } else if (thing.fast_slash) {
return '' return "";
} else { } else {
var match = thing.toString() var match = thing
.replace('\\/?', '') .toString()
.replace('(?=\\/|$)', '$') .replace("\\/?", "")
.match(/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//) .replace("(?=\\/|$)", "$")
.match(
/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//
);
return match return match
? match[1].replace(/\\(.)/g, '$1').split('/') ? match[1].replace(/\\(.)/g, "$1").split("/")
: '<complex:' + thing.toString() + '>' : "<complex:" + thing.toString() + ">";
} }
} }
// Logging.log("--- Endpoints: ---"); // Logging.log("--- Endpoints: ---");
// web.server._router.stack.forEach(print.bind(null, [])) // web.server._router.stack.forEach(print.bind(null, []))
// Logging.log("--- Endpoints end ---") // Logging.log("--- Endpoints end ---")
}).catch(e => { })
Logging.error(e) .catch((e) => {
Logging.error(e);
process.exit(); process.exit();
}) });

View File

@ -1,10 +1,10 @@
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import * as fs from "fs" import * as fs from "fs";
let private_key: string; let private_key: string;
let rsa: RSA; let rsa: RSA;
export function sign(message: Buffer): Buffer { export function sign(message: Buffer): Buffer {
return rsa.sign(message, "buffer") return rsa.sign(message, "buffer");
} }
export function verify(message: Buffer, signature: Buffer): boolean { export function verify(message: Buffer, signature: Buffer): boolean {
@ -19,28 +19,28 @@ import config from "./config";
export function createJWT(payload: any, options: jwt.SignOptions) { export function createJWT(payload: any, options: jwt.SignOptions) {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
return jwt.sign(payload, private_key, options, (err, token) => { return jwt.sign(payload, private_key, options, (err, token) => {
if (err) reject(err) if (err) reject(err);
else resolve(token) else resolve(token);
});
}); });
})
} }
export async function validateJWT(data: string) { export async function validateJWT(data: string) {
return new Promise<any>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
jwt.verify(data, public_key, (err, valid) => { jwt.verify(data, public_key, (err, valid) => {
if (err) reject(err) if (err) reject(err);
else resolve(valid) else resolve(valid);
});
}); });
})
} }
let create = false; let create = false;
if (fs.existsSync("./keys")) { if (fs.existsSync("./keys")) {
if (fs.existsSync("./keys/private.pem")) { if (fs.existsSync("./keys/private.pem")) {
if (fs.existsSync("./keys/public.pem")) { if (fs.existsSync("./keys/public.pem")) {
Logging.log("Using existing private and public key") Logging.log("Using existing private and public key");
private_key = fs.readFileSync("./keys/private.pem").toString("utf8") private_key = fs.readFileSync("./keys/private.pem").toString("utf8");
public_key = fs.readFileSync("./keys/public.pem").toString("utf8") public_key = fs.readFileSync("./keys/public.pem").toString("utf8");
if (!private_key || !public_key) { if (!private_key || !public_key) {
create = true; create = true;
@ -49,21 +49,21 @@ if (fs.existsSync("./keys")) {
} else create = true; } else create = true;
} else create = true; } else create = true;
import * as RSA from "node-rsa" import * as RSA from "node-rsa";
if (create === true) { if (create === true) {
Logging.log("Started RSA Key gen") Logging.log("Started RSA Key gen");
let rsa = new RSA({ b: 4096 }); let rsa = new RSA({ b: 4096 });
private_key = rsa.exportKey("private") private_key = rsa.exportKey("private");
public_key = rsa.exportKey("public") public_key = rsa.exportKey("public");
if (!fs.existsSync("./keys")) { if (!fs.existsSync("./keys")) {
fs.mkdirSync("./keys") fs.mkdirSync("./keys");
} }
fs.writeFileSync("./keys/private.pem", private_key) fs.writeFileSync("./keys/private.pem", private_key);
fs.writeFileSync("./keys/public.pem", public_key) fs.writeFileSync("./keys/public.pem", public_key);
Logging.log("Key pair generated") Logging.log("Key pair generated");
} }
rsa = new RSA(private_key, "private") rsa = new RSA(private_key, "private");
rsa.importKey(public_key, "public") rsa.importKey(public_key, "public");

View File

@ -4,21 +4,21 @@ import { ObjectID } from "mongodb";
import { v4 } from "uuid"; import { v4 } from "uuid";
export interface IClient extends ModelDataBase { export interface IClient extends ModelDataBase {
maintainer: ObjectID maintainer: ObjectID;
internal: boolean internal: boolean;
name: string name: string;
redirect_url: string redirect_url: string;
website: string website: string;
logo?: string logo?: string;
client_id: string client_id: string;
client_secret: string client_secret: string;
} }
const Client = DB.addModel<IClient>({ const Client = DB.addModel<IClient>({
name: "client", name: "client",
versions: [ versions: [
{ {
migration: () => { }, migration: () => {},
schema: { schema: {
maintainer: { type: ObjectID }, maintainer: { type: ObjectID },
internal: { type: Boolean, default: false }, internal: { type: Boolean, default: false },
@ -27,10 +27,10 @@ const Client = DB.addModel<IClient>({
website: { type: String }, website: { type: String },
logo: { type: String, optional: true }, logo: { type: String, optional: true },
client_id: { type: String, default: () => v4() }, client_id: { type: String, default: () => v4() },
client_secret: { type: String } client_secret: { type: String },
} },
} },
] ],
}) });
export default Client; export default Client;

View File

@ -4,24 +4,26 @@ import { ObjectID } from "mongodb";
import { v4 } from "uuid"; import { v4 } from "uuid";
export interface IClientCode extends ModelDataBase { export interface IClientCode extends ModelDataBase {
user: ObjectID user: ObjectID;
code: string; code: string;
client: ObjectID client: ObjectID;
permissions: ObjectID[] permissions: ObjectID[];
validTill: Date; validTill: Date;
} }
const ClientCode = DB.addModel<IClientCode>({ const ClientCode = DB.addModel<IClientCode>({
name: "client_code", name: "client_code",
versions: [{ versions: [
migration: () => { }, {
migration: () => {},
schema: { schema: {
user: { type: ObjectID }, user: { type: ObjectID },
code: { type: String }, code: { type: String },
client: { type: ObjectID }, client: { type: ObjectID },
permissions: { type: Array }, permissions: { type: Array },
validTill: { type: Date } validTill: { type: Date },
} },
}] },
],
}); });
export default ClientCode; export default ClientCode;

View File

@ -10,14 +10,16 @@ export interface IGrant extends ModelDataBase {
const Grant = DB.addModel<IGrant>({ const Grant = DB.addModel<IGrant>({
name: "grant", name: "grant",
versions: [{ versions: [
migration: () => { }, {
migration: () => {},
schema: { schema: {
user: { type: ObjectID }, user: { type: ObjectID },
client: { type: ObjectID }, client: { type: ObjectID },
permissions: { type: ObjectID, array: true } permissions: { type: ObjectID, array: true },
} },
}] },
}) ],
});
export default Grant; export default Grant;

View File

@ -16,27 +16,34 @@ export interface ILoginToken extends ModelDataBase {
} }
const LoginToken = DB.addModel<ILoginToken>({ const LoginToken = DB.addModel<ILoginToken>({
name: "login_token", name: "login_token",
versions: [{ versions: [
migration: () => { }, {
schema: { migration: () => {},
token: { type: String },
special: { type: Boolean, default: () => false },
user: { type: ObjectID },
validTill: { type: Date },
valid: { type: Boolean }
}
}, {
migration: (doc: ILoginToken) => { doc.validated = true; },
schema: { schema: {
token: { type: String }, token: { type: String },
special: { type: Boolean, default: () => false }, special: { type: Boolean, default: () => false },
user: { type: ObjectID }, user: { type: ObjectID },
validTill: { type: Date }, validTill: { type: Date },
valid: { type: Boolean }, valid: { type: Boolean },
validated: { type: Boolean, default: false } },
} },
}, { {
migration: (doc: ILoginToken) => { doc.validated = true; }, 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: { schema: {
token: { type: String }, token: { type: String },
special: { type: Boolean, default: () => false }, special: { type: Boolean, default: () => false },
@ -46,19 +53,21 @@ const LoginToken = DB.addModel<ILoginToken>({
validated: { type: Boolean, default: false }, validated: { type: Boolean, default: false },
data: { type: "any", optional: true }, data: { type: "any", optional: true },
ip: { type: String, optional: true }, ip: { type: String, optional: true },
browser: { type: String, optional: true } browser: { type: String, optional: true },
} },
}] },
}) ],
});
export async function CheckToken(token: ILoginToken, validated: boolean = true): Promise<boolean> { export async function CheckToken(
if (!token || !token.valid) token: ILoginToken,
return false; validated: boolean = true
if (validated && !token.validated) ): Promise<boolean> {
return false; if (!token || !token.valid) return false;
if (validated && !token.validated) return false;
if (moment().isAfter(token.validTill)) { if (moment().isAfter(token.validTill)) {
token.valid = false; token.valid = false;
await LoginToken.save(token) await LoginToken.save(token);
return false; return false;
} }
return true; return true;

View File

@ -9,14 +9,16 @@ export interface IMail extends ModelDataBase {
const Mail = DB.addModel<IMail>({ const Mail = DB.addModel<IMail>({
name: "mail", name: "mail",
versions: [{ versions: [
migration: () => { }, {
migration: () => {},
schema: { schema: {
mail: { type: String }, mail: { type: String },
verified: { type: Boolean, default: false }, verified: { type: Boolean, default: false },
primary: { type: Boolean } primary: { type: Boolean },
} },
}] },
}) ],
});
export default Mail; export default Mail;

View File

@ -11,22 +11,27 @@ export interface IPermission extends ModelDataBase {
const Permission = DB.addModel<IPermission>({ const Permission = DB.addModel<IPermission>({
name: "permission", name: "permission",
versions: [{ versions: [
migration: () => { }, {
schema: { migration: () => {},
name: { type: String },
description: { type: String },
client: { type: ObjectID }
}
}, {
migration: (old) => { old.grant_type = "user" },
schema: { schema: {
name: { type: String }, name: { type: String },
description: { type: String }, description: { type: String },
client: { type: ObjectID }, client: { type: ObjectID },
grant_type: { type: String, default: "user" } },
} },
}] {
}) 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; export default Permission;

View File

@ -14,17 +14,19 @@ export interface IRefreshToken extends ModelDataBase {
const RefreshToken = DB.addModel<IRefreshToken>({ const RefreshToken = DB.addModel<IRefreshToken>({
name: "refresh_token", name: "refresh_token",
versions: [{ versions: [
migration: () => { }, {
migration: () => {},
schema: { schema: {
token: { type: String }, token: { type: String },
user: { type: ObjectID }, user: { type: ObjectID },
client: { type: ObjectID }, client: { type: ObjectID },
permissions: { type: Array }, permissions: { type: Array },
validTill: { type: Date }, validTill: { type: Date },
valid: { type: Boolean } valid: { type: Boolean },
} },
}] },
}) ],
});
export default RefreshToken; export default RefreshToken;

View File

@ -11,15 +11,17 @@ export interface IRegCode extends ModelDataBase {
const RegCode = DB.addModel<IRegCode>({ const RegCode = DB.addModel<IRegCode>({
name: "reg_code", name: "reg_code",
versions: [{ versions: [
migration: () => { }, {
migration: () => {},
schema: { schema: {
token: { type: String }, token: { type: String },
valid: { type: Boolean }, valid: { type: Boolean },
validTill: { type: Date } validTill: { type: Date },
} },
}] },
}) ],
});
export default RegCode; export default RegCode;

View File

@ -6,7 +6,7 @@ export enum TFATypes {
OTC, OTC,
BACKUP_CODE, BACKUP_CODE,
U2F, U2F,
APP_ALLOW APP_ALLOW,
} }
export const TFANames = new Map<TFATypes, string>(); export const TFANames = new Map<TFATypes, string>();
@ -16,11 +16,11 @@ TFANames.set(TFATypes.U2F, "Security Key (U2F)");
TFANames.set(TFATypes.APP_ALLOW, "App Push"); TFANames.set(TFATypes.APP_ALLOW, "App Push");
export interface ITwoFactor extends ModelDataBase { export interface ITwoFactor extends ModelDataBase {
user: ObjectID user: ObjectID;
valid: boolean valid: boolean;
expires?: Date; expires?: Date;
name?: string; name?: string;
type: TFATypes type: TFATypes;
data: any; data: any;
} }
@ -33,7 +33,7 @@ export interface IYubiKey extends ITwoFactor {
registration?: any; registration?: any;
publicKey: string; publicKey: string;
keyHandle: string; keyHandle: string;
} };
} }
export interface IU2F extends ITwoFactor { export interface IU2F extends ITwoFactor {
@ -42,7 +42,7 @@ export interface IU2F extends ITwoFactor {
publicKey: string; publicKey: string;
keyHandle: string; keyHandle: string;
registration?: string; registration?: string;
} };
} }
export interface IBackupCode extends ITwoFactor { export interface IBackupCode extends ITwoFactor {
@ -51,8 +51,9 @@ export interface IBackupCode extends ITwoFactor {
const TwoFactor = DB.addModel<ITwoFactor>({ const TwoFactor = DB.addModel<ITwoFactor>({
name: "twofactor", name: "twofactor",
versions: [{ versions: [
migration: (e) => { }, {
migration: (e) => {},
schema: { schema: {
user: { type: ObjectID }, user: { type: ObjectID },
valid: { type: Boolean }, valid: { type: Boolean },
@ -60,8 +61,9 @@ const TwoFactor = DB.addModel<ITwoFactor>({
name: { type: String, optional: true }, name: { type: String, optional: true },
type: { type: Number }, type: { type: Number },
data: { type: "any" }, data: { type: "any" },
} },
}] },
],
}); });
export default TwoFactor; export default TwoFactor;

View File

@ -8,7 +8,7 @@ export enum Gender {
none, none,
male, male,
female, female,
other other,
} }
export interface IUser extends ModelDataBase { export interface IUser extends ModelDataBase {
@ -16,20 +16,21 @@ export interface IUser extends ModelDataBase {
username: string; username: string;
name: string; name: string;
birthday?: Date birthday?: Date;
gender: Gender; gender: Gender;
admin: boolean; admin: boolean;
password: string; password: string;
salt: string; salt: string;
mails: ObjectID[]; mails: ObjectID[];
phones: { phone: string, verified: boolean, primary: boolean }[]; phones: { phone: string; verified: boolean; primary: boolean }[];
encryption_key: string; encryption_key: string;
} }
const User = DB.addModel<IUser>({ const User = DB.addModel<IUser>({
name: "user", name: "user",
versions: [{ versions: [
migration: () => { }, {
migration: () => {},
schema: { schema: {
uid: { type: String, default: () => v4() }, uid: { type: String, default: () => v4() },
username: { type: String }, username: { type: String },
@ -46,8 +47,8 @@ const User = DB.addModel<IUser>({
type: { type: {
phone: { type: String }, phone: { type: String },
verified: { type: Boolean }, verified: { type: Boolean },
primary: { type: Boolean } primary: { type: Boolean },
} },
}, },
twofactor: { twofactor: {
array: true, array: true,
@ -55,48 +56,15 @@ const User = DB.addModel<IUser>({
type: { type: {
token: { type: String }, token: { type: String },
valid: { type: Boolean }, valid: { type: Boolean },
type: { type: Number } 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 }
}
}, },
twofactor: {
array: true,
model: true,
type: {
token: { type: String },
valid: { type: Boolean },
type: { type: Number }
}
}, },
encryption_key: {
type: String,
default: () => randomString(64)
}
}
}, },
{ {
migration: (e: any) => { delete e.twofactor }, migration: (e: IUser) => {
e.encryption_key = randomString(64);
},
schema: { schema: {
uid: { type: String, default: () => v4() }, uid: { type: String, default: () => v4() },
username: { type: String }, username: { type: String },
@ -113,15 +81,54 @@ const User = DB.addModel<IUser>({
type: { type: {
phone: { type: String }, phone: { type: String },
verified: { type: Boolean }, verified: { type: Boolean },
primary: { type: Boolean } primary: { type: Boolean },
} },
},
twofactor: {
array: true,
model: true,
type: {
token: { type: String },
valid: { type: Boolean },
type: { type: Number },
},
}, },
encryption_key: { encryption_key: {
type: String, type: String,
default: () => randomString(64) 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 },
},
},
encryption_key: {
type: String,
default: () => randomString(64),
},
},
},
],
});
export default User; export default User;

View File

@ -8,7 +8,6 @@ import { ObjectID } from "bson";
import DB from "./database"; import DB from "./database";
import TwoFactor from "./models/twofactor"; import TwoFactor from "./models/twofactor";
import * as speakeasy from "speakeasy"; import * as speakeasy from "speakeasy";
import LoginToken from "./models/login_token"; import LoginToken from "./models/login_token";
import Mail from "./models/mail"; import Mail from "./models/mail";
@ -21,13 +20,12 @@ export default async function TestData() {
mail = Mail.new({ mail = Mail.new({
mail: "test@test.de", mail: "test@test.de",
primary: true, primary: true,
verified: true verified: true,
}) });
await Mail.save(mail); await Mail.save(mail);
} }
let u = await User.findOne({ username: "test" }); let u = await User.findOne({ username: "test" });
if (!u) { if (!u) {
Logging.log("Adding test user"); Logging.log("Adding test user");
@ -36,21 +34,22 @@ export default async function TestData() {
birthday: new Date(), birthday: new Date(),
gender: Gender.male, gender: Gender.male,
name: "Test Test", name: "Test Test",
password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc", password:
"125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
salt: "test", salt: "test",
admin: true, admin: true,
phones: [ phones: [
{ phone: "+4915962855955", primary: true, verified: true }, { 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); await User.save(u);
} }
let c = await Client.findOne({ client_id: "test001" }); let c = await Client.findOne({ client_id: "test001" });
if (!c) { if (!c) {
Logging.log("Adding test client") Logging.log("Adding test client");
c = Client.new({ c = Client.new({
client_id: "test001", client_id: "test001",
client_secret: "test001", client_secret: "test001",
@ -58,19 +57,19 @@ export default async function TestData() {
maintainer: u._id, maintainer: u._id,
name: "Test Client", name: "Test Client",
website: "http://example.com", website: "http://example.com",
redirect_url: "http://example.com" redirect_url: "http://example.com",
}) });
await Client.save(c); await Client.save(c);
} }
let perm = await Permission.findOne({ id: 0 }); let perm = await Permission.findOne({ id: 0 });
if (!perm) { if (!perm) {
Logging.log("Adding test permission") Logging.log("Adding test permission");
perm = Permission.new({ perm = Permission.new({
_id: new ObjectID("507f1f77bcf86cd799439011"), _id: new ObjectID("507f1f77bcf86cd799439011"),
name: "TestPerm", name: "TestPerm",
description: "Permission just for testing purposes", description: "Permission just for testing purposes",
client: c._id client: c._id,
}); });
await (await (Permission as any)._collection).insertOne(perm); await (await (Permission as any)._collection).insertOne(perm);
@ -80,30 +79,29 @@ export default async function TestData() {
let r = await RegCode.findOne({ token: "test" }); let r = await RegCode.findOne({ token: "test" });
if (!r) { if (!r) {
Logging.log("Adding test reg_code") Logging.log("Adding test reg_code");
r = RegCode.new({ r = RegCode.new({
token: "test", token: "test",
valid: true, valid: true,
validTill: moment().add("1", "year").toDate() validTill: moment().add("1", "year").toDate(),
}) });
await RegCode.save(r); 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) { if (!t) {
t = TwoFactor.new({ t = TwoFactor.new({
user: u._id, user: u._id,
type: 0, type: 0,
valid: true, valid: true,
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ", data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
expires: null expires: null,
}) });
TwoFactor.save(t); TwoFactor.save(t);
} }
let login_token = await LoginToken.findOne({ token: "test01" }); let login_token = await LoginToken.findOne({ token: "test01" });
if (login_token) if (login_token) await LoginToken.delete(login_token);
await LoginToken.delete(login_token);
login_token = LoginToken.new({ login_token = LoginToken.new({
browser: "DEMO", browser: "DEMO",
@ -113,13 +111,12 @@ export default async function TestData() {
valid: true, valid: true,
validTill: moment().add("10", "years").toDate(), validTill: moment().add("10", "years").toDate(),
user: u._id, user: u._id,
validated: true validated: true,
}); });
await LoginToken.save(login_token); await LoginToken.save(login_token);
let special_token = await LoginToken.findOne({ token: "test02" }); let special_token = await LoginToken.findOne({ token: "test02" });
if (special_token) if (special_token) await LoginToken.delete(special_token);
await LoginToken.delete(special_token);
special_token = LoginToken.new({ special_token = LoginToken.new({
browser: "DEMO", browser: "DEMO",
@ -129,11 +126,10 @@ export default async function TestData() {
valid: true, valid: true,
validTill: moment().add("10", "years").toDate(), validTill: moment().add("10", "years").toDate(),
user: u._id, user: u._id,
validated: true validated: true,
}); });
await LoginToken.save(special_token); await LoginToken.save(special_token);
// setInterval(() => { // setInterval(() => {
// let code = speakeasy.totp({ // let code = speakeasy.totp({
// secret: t.data, // secret: t.data,

View File

@ -1,6 +1,6 @@
import * as handlebars from "handlebars" import * as handlebars from "handlebars";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { __ as i__ } from "i18n" import { __ as i__ } from "i18n";
import config from "../config"; import config from "../config";
let template: handlebars.TemplateDelegate<any>; let template: handlebars.TemplateDelegate<any>;
@ -11,11 +11,11 @@ function loadStatic() {
export default function GetAdminPage(__: typeof i__): string { export default function GetAdminPage(__: typeof i__): string {
if (config.core.dev) { if (config.core.dev) {
loadStatic() loadStatic();
} }
let data = {} let data = {};
return template(data, { helpers: { "i18n": __ } }) return template(data, { helpers: { i18n: __ } });
} }
loadStatic() loadStatic();

View File

@ -1,26 +1,34 @@
import { compile, TemplateDelegate } from "handlebars" import { compile, TemplateDelegate } from "handlebars";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { __ as i__ } from "i18n" import { __ as i__ } from "i18n";
import config from "../config"; import config from "../config";
let template: TemplateDelegate<any>; let template: TemplateDelegate<any>;
function loadStatic() { function loadStatic() {
let html = readFileSync("./views/out/authorize/authorize.html").toString(); 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) { if (config.core.dev) {
loadStatic() loadStatic();
} }
return template({ return template(
{
title: __("Authorize %s", appname), title: __("Authorize %s", appname),
information: __("By clicking on ALLOW, you allow this app to access the requested recources."), information: __(
"By clicking on ALLOW, you allow this app to access the requested recources."
),
scopes: scopes, scopes: scopes,
// request: request // request: request
}, { helpers: { "i18n": __ } }); },
{ helpers: { i18n: __ } }
);
} }
loadStatic() loadStatic();

View File

@ -1,6 +1,6 @@
import * as handlebars from "handlebars" import * as handlebars from "handlebars";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { __ as i__ } from "i18n" import { __ as i__ } from "i18n";
import config from "../config"; import config from "../config";
let template: handlebars.TemplateDelegate<any>; let template: handlebars.TemplateDelegate<any>;
@ -29,11 +29,11 @@ function loadStatic() {
export default function GetLoginPage(__: typeof i__): string { export default function GetLoginPage(__: typeof i__): string {
if (config.core.dev) { if (config.core.dev) {
loadStatic() loadStatic();
} }
let data = {} let data = {};
return template(data, { helpers: { "i18n": __ } }); return template(data, { helpers: { i18n: __ } });
} }
loadStatic() loadStatic();

View File

@ -1,6 +1,6 @@
import * as handlebars from "handlebars" import * as handlebars from "handlebars";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { __ as i__ } from "i18n" import { __ as i__ } from "i18n";
import config from "../config"; import config from "../config";
let template: handlebars.TemplateDelegate<any>; let template: handlebars.TemplateDelegate<any>;
@ -11,11 +11,11 @@ function loadStatic() {
export default function GetRegistrationPage(__: typeof i__): string { export default function GetRegistrationPage(__: typeof i__): string {
if (config.core.dev) { if (config.core.dev) {
loadStatic() loadStatic();
} }
let data = {} let data = {};
return template(data, { helpers: { "i18n": __ } }) return template(data, { helpers: { i18n: __ } });
} }
loadStatic() loadStatic();

View File

@ -1,23 +1,17 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Basic Options */ /* Basic Options */
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "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'. */ "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"declaration": true, /* Generates corresponding '.d.ts' file. */ "declaration": true /* Generates corresponding '.d.ts' file. */,
"sourceMap": true, /* Generates corresponding '.map' file. */ "sourceMap": true /* Generates corresponding '.map' file. */,
"outDir": "./lib", /* Redirect output structure to the directory. */ "outDir": "./lib" /* Redirect output structure to the directory. */,
"strict": false, /* Enable all strict type-checking options. */ "strict": false /* Enable all strict type-checking options. */,
"preserveWatchOutput": true, "preserveWatchOutput": true,
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": [ "exclude": ["node_modules/"],
"node_modules/" "files": ["src/express.d.ts"],
], "include": ["./src"]
"files": [
"src/express.d.ts"
],
"include": [
"./src"
]
} }

View File

@ -5,49 +5,45 @@ const {
copyFileSync, copyFileSync,
writeFileSync, writeFileSync,
readFileSync, readFileSync,
exists exists,
} = require('fs') } = require("fs");
const { const { join, basename, dirname } = require("path");
join,
basename,
dirname
} = require('path')
const isDirectory = (source) => lstatSync(source).isDirectory();
const getDirectories = (source) =>
const isDirectory = source => lstatSync(source).isDirectory() readdirSync(source)
const getDirectories = source => .map((name) => join(source, name))
readdirSync(source).map(name => join(source, name)).filter(isDirectory) .filter(isDirectory);
function ensureDir(folder) { function ensureDir(folder) {
try { try {
if (!isDirectory(folder)) mkdirSync(folder) if (!isDirectory(folder)) mkdirSync(folder);
} catch (e) { } 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))); const sass = require("sass");
ensureDir("./out")
const sass = require('sass');
function findHead(elm) { function findHead(elm) {
if (elm.tagName === "head") return elm; if (elm.tagName === "head") return elm;
for (let i = 0; i < elm.childNodes.length; i++) { 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; if (res) return res;
} }
return undefined; return undefined;
} }
const rollup = require("rollup") const rollup = require("rollup");
const includepaths = require("rollup-plugin-includepaths") const includepaths = require("rollup-plugin-includepaths");
const typescript = require("rollup-plugin-typescript2"); const typescript = require("rollup-plugin-typescript2");
const resolve = require("rollup-plugin-node-resolve"); const resolve = require("rollup-plugin-node-resolve");
const minify = require("html-minifier").minify const minify = require("html-minifier").minify;
const gzipSize = require('gzip-size'); const gzipSize = require("gzip-size");
async function file_name(folder, name, exts) { async function file_name(folder, name, exts) {
for (let ext of exts) { for (let ext of exts) {
@ -61,58 +57,57 @@ async function buildPage(folder) {
const pagename = basename(folder); const pagename = basename(folder);
const outpath = "./out/" + pagename; const outpath = "./out/" + pagename;
ensureDir(outpath) ensureDir(outpath);
const basefile = await file_name(folder, pagename, ["tsx", "ts", "js"]); const basefile = await file_name(folder, pagename, ["tsx", "ts", "js"]);
let bundle = await rollup.rollup({ let bundle = await rollup.rollup({
input: basefile, input: basefile,
plugins: [ plugins: [
includepaths({ includepaths({
paths: ["shared", "node_modules"] paths: ["shared", "node_modules"],
}), }),
typescript(), typescript(),
resolve({ resolve({
// not all files you want to resolve are .js files // 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 // whether to prefer built-in modules (e.g. `fs`, `path`) or
// local ones with the same names // local ones with the same names
preferBuiltins: false, // Default: true preferBuiltins: false, // Default: true
}) }),
], ],
treeshake: true treeshake: true,
}) });
let { output } = await bundle.generate({ let { output } = await bundle.generate({
format: "iife", format: "iife",
compact: true compact: true,
}) });
let { code } = output[0]; let { code } = output[0];
let sass_res = sass.renderSync({ let sass_res = sass.renderSync({
file: folder + `/${pagename}.scss`, file: folder + `/${pagename}.scss`,
includePaths: ["./node_modules", folder, "./shared"], includePaths: ["./node_modules", folder, "./shared"],
outputStyle: "compressed" outputStyle: "compressed",
}) });
let css = "<style>\n" + sass_res.css.toString("utf8") + "\n</style>\n"; let css = "<style>\n" + sass_res.css.toString("utf8") + "\n</style>\n";
let script = "<script>\n" + code + "\n</script>\n"; let script = "<script>\n" + code + "\n</script>\n";
let html = readFileSync(`${folder}/${pagename}.hbs`).toString("utf8"); let html = readFileSync(`${folder}/${pagename}.hbs`).toString("utf8");
let idx = html.indexOf("</head>") let idx = html.indexOf("</head>");
if (idx < 0) throw new Error("No head element found") if (idx < 0) throw new Error("No head element found");
let idx2 = html.indexOf("</body>") let idx2 = html.indexOf("</body>");
if (idx2 < 0) throw new Error("No body element found") if (idx2 < 0) throw new Error("No body element found");
if (idx < idx2) { if (idx < idx2) {
let part1 = html.slice(0, idx) let part1 = html.slice(0, idx);
let part2 = html.slice(idx, idx2); let part2 = html.slice(idx, idx2);
let part3 = html.slice(idx2, html.length); let part3 = html.slice(idx2, html.length);
html = part1 + css + part2 + script + part3; html = part1 + css + part2 + script + part3;
} else { } else {
let part1 = html.slice(0, idx2) let part1 = html.slice(0, idx2);
let part2 = html.slice(idx2, idx); let part2 = html.slice(idx2, idx);
let part3 = html.slice(idx, html.length); let part3 = html.slice(idx, html.length);
html = part1 + script + part2 + css + part3; html = part1 + script + part2 + css + part3;
@ -126,45 +121,50 @@ async function buildPage(folder) {
minifyCSS: false, minifyCSS: false,
minifyJS: false, minifyJS: false,
removeComments: true, removeComments: true,
useShortDoctype: true useShortDoctype: true,
}) });
let gzips = await gzipSize(result) let gzips = await gzipSize(result);
writeFileSync(`${outpath}/${pagename}.html`, result) writeFileSync(`${outpath}/${pagename}.html`, result);
let stats = { let stats = {
sass: sass_res.stats, sass: sass_res.stats,
js: { js: {
chars: code.length chars: code.length,
}, },
css: { css: {
chars: css.length chars: css.length,
}, },
bundle_size: result.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() { async function run() {
console.log("Start compiling!"); console.log("Start compiling!");
let pages = getDirectories("./src"); let pages = getDirectories("./src");
await Promise.all(pages.map(async e => { await Promise.all(
pages.map(async (e) => {
try { try {
await buildPage(e) await buildPage(e);
} catch (er) { } catch (er) {
console.error("Failed compiling", basename(e)) console.error("Failed compiling", basename(e));
console.log(er) console.log(er);
} }
})) })
console.log("Finished compiling!") );
console.log("Finished compiling!");
} }
const chokidar = require("chokidar"); const chokidar = require("chokidar");
if (process.argv.join(" ").toLowerCase().indexOf("watch") >= 0) if (process.argv.join(" ").toLowerCase().indexOf("watch") >= 0)
chokidar.watch(["./src", "./node_modules", "./package.json", "./package-lock.json"], { chokidar
ignoreInitial: true .watch(
}) ["./src", "./node_modules", "./package.json", "./package-lock.json"],
{
ignoreInitial: true,
}
)
.on("all", () => run()); .on("all", () => run());
run() run();

View File

@ -1,15 +1,15 @@
export function setCookie(cname, cvalue, exdate) { export function setCookie(cname, cvalue, exdate) {
var expires = exdate ? `;expires=${exdate}` : ""; var expires = exdate ? `;expires=${exdate}` : "";
document.cookie = `${cname}=${cvalue}${expires}` document.cookie = `${cname}=${cvalue}${expires}`;
} }
export function getCookie(cname) { export function getCookie(cname) {
var name = cname + "="; var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie); var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';'); var ca = decodedCookie.split(";");
for (var i = 0; i < ca.length; i++) { for (var i = 0; i < ca.length; i++) {
var c = ca[i]; var c = ca[i];
while (c.charAt(0) == ' ') { while (c.charAt(0) == " ") {
c = c.substring(1); c = c.substring(1);
} }
if (c.indexOf(name) == 0) { if (c.indexOf(name) == 0) {

View File

@ -2,9 +2,8 @@ export default function fireEvent(element, event) {
if (document.createEventObject) { if (document.createEventObject) {
// dispatch for IE // dispatch for IE
var evt = document.createEventObject(); var evt = document.createEventObject();
return element.fireEvent('on' + event, evt) return element.fireEvent("on" + event, evt);
} } else {
else {
// dispatch for firefox + others // dispatch for firefox + others
var evt = document.createEvent("HTMLEvents"); var evt = document.createEvent("HTMLEvents");
evt.initEvent(event, true, true); // event type,bubbling,cancelable evt.initEvent(event, true, true); // event type,bubbling,cancelable

View File

@ -1,14 +1,18 @@
export default function getFormData(element) { export default function getFormData(element) {
let data = {}; 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 (typeof element.name === "string") {
if (element.type === "checkbox") data[element.name] = element.checked; if (element.type === "checkbox") data[element.name] = element.checked;
else data[element.name] = element.value; else data[element.name] = element.value;
} }
} }
element.childNodes.forEach(child => { element.childNodes.forEach((child) => {
let res = getFormData(child); let res = getFormData(child);
data = Object.assign(data, res); data = Object.assign(data, res);
}) });
return data; return data;
} }

View File

@ -1,24 +1,24 @@
(() => { (() => {
const run = () => { const run = () => {
document.querySelectorAll(".floating>input").forEach(e => { document.querySelectorAll(".floating>input").forEach((e) => {
function checkState() { function checkState() {
if (e.value !== "") { if (e.value !== "") {
if (e.classList.contains("used")) return; if (e.classList.contains("used")) return;
e.classList.add("used") e.classList.add("used");
} else { } else {
if (e.classList.contains("used")) e.classList.remove("used") if (e.classList.contains("used")) e.classList.remove("used");
} }
} }
e.addEventListener("change", () => checkState()) e.addEventListener("change", () => checkState());
checkState() checkState();
}) });
} };
run(); run();
var mutationObserver = new MutationObserver(() => { var mutationObserver = new MutationObserver(() => {
run() run();
}); });
mutationObserver.observe(document.documentElement, { mutationObserver.observe(document.documentElement, {
@ -28,6 +28,6 @@
subtree: true, subtree: true,
}); });
window.Mutt window.Mutt;
window.addEventListener("DOMNodeInserted", () => run()) window.addEventListener("DOMNodeInserted", () => run());
})(); })();

View File

@ -5,7 +5,7 @@
min-height: 45px; min-height: 45px;
} }
.floating>input { .floating > input {
font-size: 18px; font-size: 18px;
padding: 10px 10px 10px 5px; padding: 10px 10px 10px 5px;
appearance: none; appearance: none;
@ -19,13 +19,13 @@
border-bottom: 1px solid #757575; border-bottom: 1px solid #757575;
} }
.floating>input:focus { .floating > input:focus {
outline: none; outline: none;
} }
/* Label */ /* Label */
.floating>label { .floating > label {
color: #999; color: #999;
font-size: 18px; font-size: 18px;
font-weight: normal; font-weight: normal;
@ -38,10 +38,10 @@
/* active */ /* active */
.floating>input:focus~label, .floating > input:focus ~ label,
.floating>input.used~label { .floating > input.used ~ label {
top: -.75em; top: -0.75em;
transform: scale(.75); transform: scale(0.75);
left: -2px; left: -2px;
/* font-size: 14px; */ /* font-size: 14px; */
color: $primary; color: $primary;
@ -58,7 +58,7 @@
.bar:before, .bar:before,
.bar:after { .bar:after {
content: ''; content: "";
height: 2px; height: 2px;
width: 0; width: 0;
bottom: 1px; bottom: 1px;
@ -77,8 +77,8 @@
/* active */ /* active */
.floating>input:focus~.bar:before, .floating > input:focus ~ .bar:before,
.floating>input:focus~.bar:after { .floating > input:focus ~ .bar:after {
width: 50%; width: 50%;
} }
@ -96,7 +96,7 @@
/* active */ /* active */
.floating>input:focus~.highlight { .floating > input:focus ~ .highlight {
animation: inputHighlighter 0.3s ease; animation: inputHighlighter 0.3s ease;
} }

File diff suppressed because one or more lines are too long

View File

@ -1,16 +1,26 @@
export default function request(endpoint, method, data) { export default function request(endpoint, method, data) {
var headers = new Headers(); var headers = new Headers();
headers.set('Content-Type', 'application/json'); headers.set("Content-Type", "application/json");
return fetch(endpoint, { return fetch(endpoint, {
method: method, method: method,
body: JSON.stringify(data), body: JSON.stringify(data),
headers: headers, headers: headers,
credentials: "include" credentials: "include",
}).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;
}) })
.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;
});
} }

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// $primary: #4a89dc; // $primary: #4a89dc;
$primary: #1E88E5; $primary: #1e88e5;
$error: #ff2f00; $error: #ff2f00;
.btn-primary { .btn-primary {
color: white !important; color: white !important;

View File

@ -26,9 +26,8 @@ Handlebars.registerHelper("formatDate", function (datetime, format) {
document.getElementById("sitename").innerText = title; document.getElementById("sitename").innerText = title;
} }
const cc = document.getElementById("custom_data");
const cc = document.getElementById("custom_data") const ccc = document.getElementById("custom_data_cont");
const ccc = document.getElementById("custom_data_cont")
function setCustomCard(content) { function setCustomCard(content) {
if (!content) { if (!content) {
@ -40,8 +39,8 @@ Handlebars.registerHelper("formatDate", function (datetime, format) {
} }
} }
const error_cont = document.getElementById("error_cont") const error_cont = document.getElementById("error_cont");
const error_msg = document.getElementById("error_msg") const error_msg = document.getElementById("error_msg");
function catchError(error) { function catchError(error) {
error_cont.style.display = ""; error_cont.style.display = "";
@ -50,40 +49,53 @@ Handlebars.registerHelper("formatDate", function (datetime, format) {
} }
async function renderUser() { async function renderUser() {
console.log("Rendering User") console.log("Rendering User");
setTitle("User") setTitle("User");
const listt = Handlebars.compile(document.getElementById("template-user-list").innerText) const listt = Handlebars.compile(
document.getElementById("template-user-list").innerText
);
async function loadList() { async function loadList() {
let data = await request("/api/admin/user", "GET"); let data = await request("/api/admin/user", "GET");
tableb.innerHTML = listt({ tableb.innerHTML = listt({
users: data users: data,
}) });
} }
window.userOnChangeType = (id) => { 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) => { 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(); await loadList();
} }
async function renderPermissions(client_id, client_name) { async function renderPermissions(client_id, client_name) {
const listt = Handlebars.compile(document.getElementById("template-permission-list").innerText); const listt = Handlebars.compile(
const formt = Handlebars.compile(document.getElementById("template-permission-form").innerText); document.getElementById("template-permission-list").innerText
);
const formt = Handlebars.compile(
document.getElementById("template-permission-form").innerText
);
setCustomCard(); setCustomCard();
async function loadList() { async function loadList() {
try { 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({ tableb.innerHTML = listt({
client_id: client_id, client_id: client_id,
client_name: client_name, client_name: client_name,
permissions: data permissions: data,
}) });
} catch (err) { } catch (err) {
catchError(err); catchError(err);
} }
@ -91,11 +103,13 @@ Handlebars.registerHelper("formatDate", function (datetime, format) {
window.gotoClients = () => { window.gotoClients = () => {
renderClient(); renderClient();
} };
window.deletePermission = (id) => { 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 = () => { window.createPermission = () => {
try { try {
@ -103,41 +117,49 @@ Handlebars.registerHelper("formatDate", function (datetime, format) {
} catch (err) { } catch (err) {
console.log("Err", err); console.log("Err", err);
} }
} };
window.createPermissionSubmit = (elm) => { window.createPermissionSubmit = (elm) => {
console.log(elm); console.log(elm);
let data = getFormData(elm); let data = getFormData(elm);
console.log(data); console.log(data);
request("/api/admin/permission", "POST", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError) request("/api/admin/permission", "POST", data)
} .then(() => setCustomCard())
await loadList() .then(() => loadList())
.catch(catchError);
};
await loadList();
} }
async function renderClient() { async function renderClient() {
console.log("Rendering Client") console.log("Rendering Client");
setTitle("Client") setTitle("Client");
const listt = Handlebars.compile(document.getElementById("template-client-list").innerText) const listt = Handlebars.compile(
const formt = Handlebars.compile(document.getElementById("template-client-form").innerText) document.getElementById("template-client-list").innerText
);
const formt = Handlebars.compile(
document.getElementById("template-client-form").innerText
);
let clients = []; let clients = [];
async function loadList() { async function loadList() {
let data = await request("/api/admin/client", "GET"); let data = await request("/api/admin/client", "GET");
clients = data; clients = data;
tableb.innerHTML = listt({ tableb.innerHTML = listt({
clients: data clients: data,
}) });
} }
window.permissionsClient = (id) => { window.permissionsClient = (id) => {
renderPermissions(id, clients.find(e => e._id === id).name); renderPermissions(id, clients.find((e) => e._id === id).name);
} };
window.deleteClient = (id) => { 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) => { window.createClientSubmit = (elm) => {
console.log(elm); console.log(elm);
@ -146,45 +168,57 @@ Handlebars.registerHelper("formatDate", function (datetime, format) {
let id = data.id; let id = data.id;
delete data.id; delete data.id;
if (id && 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 { } 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 = () => { window.createClient = () => {
setCustomCard(formt()); setCustomCard(formt());
} };
window.editClient = (id) => { window.editClient = (id) => {
let client = clients.find(e => e._id === id); let client = clients.find((e) => e._id === id);
if (!client) return catchError(new Error("Client does not exist!!")) if (!client) return catchError(new Error("Client does not exist!!"));
setCustomCard(formt(client)); setCustomCard(formt(client));
} };
await loadList().catch(catchError); await loadList().catch(catchError);
} }
async function renderRegCode() { async function renderRegCode() {
console.log("Rendering RegCode") console.log("Rendering RegCode");
setTitle("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() { async function loadList() {
let data = await request("/api/admin/regcode", "GET"); let data = await request("/api/admin/regcode", "GET");
tableb.innerHTML = listt({ tableb.innerHTML = listt({
regcodes: data regcodes: data,
}) });
} }
window.deleteRegcode = (id) => { 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 = () => { window.createRegcode = () => {
request("/api/admin/regcode", "POST").then(() => loadList()).catch(catchError); request("/api/admin/regcode", "POST")
} .then(() => loadList())
.catch(catchError);
};
await 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"); const type = new URL(window.location.href).searchParams.get("type");
switch (type) { switch (type) {
case "client": case "client":
renderClient().catch(catchError) renderClient().catch(catchError);
break break;
case "regcode": case "regcode":
renderRegCode().catch(catchError) renderRegCode().catch(catchError);
break; break;
case "user": case "user":
default: default:
renderUser().catch(catchError); renderUser().catch(catchError);
break; break;
} }
})() })();

View File

@ -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() { function submit() {
document.getElementById("hidden_form").submit(); document.getElementById("hidden_form").submit();
@ -10,9 +12,10 @@ document.getElementById("cancel").onclick = () => {
if (uri === "$local") { if (uri === "$local") {
uri = "/code"; 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 = () => { document.getElementById("allow").onclick = () => {
submit() submit();
} };

View File

@ -22,12 +22,17 @@ body {
} }
.title { .title {
text-align:center; text-align: center;
} }
h1, h3 { font-weight: 300; } h1,
h3 {
font-weight: 300;
}
h1 { color: #636363; } h1 {
color: #636363;
}
ul { ul {
list-style: none; list-style: none;
@ -75,5 +80,6 @@ ul {
padding: 3em 2em 2em 2em; padding: 3em 2em 2em 2em;
background: #fafafa; background: #fafafa;
border: 1px solid #ebebeb; 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;
} }

View File

@ -1,114 +1,123 @@
import sha from "sha512"; import sha from "sha512";
import { import { setCookie, getCookie } from "cookie";
setCookie, import "inputs";
getCookie
} from "cookie"
import "inputs"
const loader = document.getElementById("loader") const loader = document.getElementById("loader");
const container = document.getElementById("container") const container = document.getElementById("container");
const usernameinput = document.getElementById("username") const usernameinput = document.getElementById("username");
const usernamegroup = document.getElementById("usernamegroup") const usernamegroup = document.getElementById("usernamegroup");
const uerrorfield = document.getElementById("uerrorfield") const uerrorfield = document.getElementById("uerrorfield");
const passwordinput = document.getElementById("password") const passwordinput = document.getElementById("password");
const passwordgroup = document.getElementById("passwordgroup") const passwordgroup = document.getElementById("passwordgroup");
const perrorfield = document.getElementById("perrorfield") const perrorfield = document.getElementById("perrorfield");
const nextbutton = document.getElementById("nextbutton") const nextbutton = document.getElementById("nextbutton");
const loginbutton = document.getElementById("loginbutton") const loginbutton = document.getElementById("loginbutton");
let username; let username;
let salt; let salt;
usernameinput.focus() usernameinput.focus();
const loading = () => { const loading = () => {
container.style.filter = "blur(2px)"; container.style.filter = "blur(2px)";
loader.style.display = ""; loader.style.display = "";
} };
const loading_fin = () => { const loading_fin = () => {
container.style.filter = "" container.style.filter = "";
loader.style.display = "none"; loader.style.display = "none";
} };
loading_fin(); loading_fin();
usernameinput.onkeydown = (e) => { usernameinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which; var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) nextbutton.click(); if (keycode === 13) nextbutton.click();
clearError(uerrorfield); clearError(uerrorfield);
} };
nextbutton.onclick = async () => { nextbutton.onclick = async () => {
loading(); loading();
username = usernameinput.value; username = usernameinput.value;
try { try {
let res = await fetch("/api/user/login?type=username&username=" + username, { let res = await fetch(
method: "POST" "/api/user/login?type=username&username=" + username,
}).then(e => { {
if (e.status !== 200) throw new Error(e.statusText) method: "POST",
return e.json() }
}).then(data => { )
.then((e) => {
if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) { if (data.error) {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error));
} }
return data; return data;
}) });
salt = res.salt; salt = res.salt;
usernamegroup.classList.add("invisible") usernamegroup.classList.add("invisible");
passwordgroup.classList.remove("invisible") passwordgroup.classList.remove("invisible");
passwordinput.focus() passwordinput.focus();
} catch (e) { } catch (e) {
showError(uerrorfield, e.message) showError(uerrorfield, e.message);
} }
loading_fin() loading_fin();
} };
passwordinput.onkeydown = (e) => { passwordinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which; var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) loginbutton.click(); if (keycode === 13) loginbutton.click();
clearError(perrorfield); clearError(perrorfield);
} };
loginbutton.onclick = async () => { loginbutton.onclick = async () => {
loading(); loading();
let pw = sha(salt + passwordinput.value); let pw = sha(salt + passwordinput.value);
try { try {
let { login, special, tfa } = await fetch("/api/user/login?type=password", { let { login, special, tfa } = await fetch(
"/api/user/login?type=password",
{
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
username: usernameinput.value, username: usernameinput.value,
password: pw password: pw,
}), }),
headers: { headers: {
'content-type': 'application/json' "content-type": "application/json",
}, },
}).then(e => { }
if (e.status !== 200) throw new Error(e.statusText) )
return e.json() .then((e) => {
}).then(data => { if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) { if (data.error) {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error));
} }
return data; return data;
}) });
setCookie("login", login.token, new Date(login.expires).toUTCString()); setCookie("login", login.token, new Date(login.expires).toUTCString());
setCookie("special", special.token, new Date(special.expires).toUTCString()); setCookie(
let d = new Date() "special",
d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000)); //Keep the username 30 days 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()); setCookie("username", username, d.toUTCString());
let url = new URL(window.location.href); let url = new URL(window.location.href);
let state = url.searchParams.get("state") let state = url.searchParams.get("state");
let red = "/" let red = "/";
if (tfa) twofactor(tfa); if (tfa) twofactor(tfa);
else { else {
if (state) { if (state) {
let base64 = url.searchParams.get("base64") let base64 = url.searchParams.get("base64");
if (base64) if (base64) red = atob(state);
red = atob(state) else red = state;
else
red = state
} }
window.location.href = red; window.location.href = red;
} }
@ -117,19 +126,19 @@ loginbutton.onclick = async () => {
showError(perrorfield, e.message); showError(perrorfield, e.message);
} }
loading_fin(); loading_fin();
} };
function clearError(field) { function clearError(field) {
field.innerText = ""; field.innerText = "";
field.classList.add("invisible") field.classList.add("invisible");
} }
function showError(field, error) { function showError(field, error) {
field.innerText = error; field.innerText = error;
field.classList.remove("invisible") field.classList.remove("invisible");
} }
username = getCookie("username") username = getCookie("username");
if (username) { if (username) {
usernameinput.value = username; usernameinput.value = username;
@ -138,10 +147,9 @@ if (username) {
usernameinput.dispatchEvent(evt); usernameinput.dispatchEvent(evt);
} }
function twofactor(tfa) { function twofactor(tfa) {
let list = tfa let list = tfa
.map(entry => { .map((entry) => {
switch (entry) { switch (entry) {
case 0: // OTC case 0: // OTC
return "Authenticator App"; return "Authenticator App";
@ -150,7 +158,7 @@ function twofactor(tfa) {
} }
return undefined; return undefined;
}) })
.filter(e => e !== undefined) .filter((e) => e !== undefined)
.reduce((p, c) => p + `<li>${c}</li>`, ""); .reduce((p, c) => p + `<li>${c}</li>`, "");
let tfl = document.getElementById("tflist"); let tfl = document.getElementById("tflist");

View File

@ -41,7 +41,8 @@ form {
padding: 3em 2em 2em 2em; padding: 3em 2em 2em 2em;
background: #fafafa; background: #fafafa;
border: 1px solid #ebebeb; 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; position: relative;
} }
@ -50,10 +51,13 @@ form {
height: 64px; height: 64px;
margin: auto; margin: auto;
position: absolute; position: absolute;
top: 0; left: 0; bottom: 0; right: 0; top: 0;
left: 0;
bottom: 0;
right: 0;
} }
.loader{ .loader {
display: inline-block; display: inline-block;
position: relative; position: relative;
z-index: 100; z-index: 100;
@ -78,7 +82,6 @@ form {
} }
} }
footer { footer {
text-align: center; text-align: center;
} }
@ -86,13 +89,13 @@ footer {
footer p { footer p {
color: #888; color: #888;
font-size: 13px; font-size: 13px;
letter-spacing: .4px; letter-spacing: 0.4px;
} }
footer a { footer a {
color: $primary; color: $primary;
text-decoration: none; text-decoration: none;
transition: all .2s ease; transition: all 0.2s ease;
} }
footer a:hover { footer a:hover {
@ -102,11 +105,11 @@ footer a:hover {
footer img { footer img {
width: 80px; width: 80px;
transition: all .2s ease; transition: all 0.2s ease;
} }
footer img:hover { footer img:hover {
opacity: .83; opacity: 0.83;
} }
footer img:focus, footer img:focus,

View File

@ -1,68 +1,96 @@
import { h, Component, render } from "preact" import { h, Component, render } from "preact";
import "inputs" import "inputs";
import "./u2f-api-polyfill" import "./u2f-api-polyfill";
import sha from "sha512"; import sha from "sha512";
import { import { setCookie, getCookie } from "cookie";
setCookie,
getCookie
} from "cookie"
let appname = "test"; let appname = "test";
function Loader() { function Loader() {
return <div class="loader_box" id="loader"> return (
<div class="loader_box" id="loader">
<div class="loader"></div> <div class="loader"></div>
</div> </div>
);
} }
class Username extends Component<{ username: string, onNext: (username: string, salt: string) => void }, { error: string, loading: boolean }> { class Username extends Component<
{ username: string; onNext: (username: string, salt: string) => void },
{ error: string; loading: boolean }
> {
username_input: HTMLInputElement; username_input: HTMLInputElement;
constructor() { constructor() {
super(); super();
this.state = { error: undefined, loading: false } this.state = { error: undefined, loading: false };
} }
async onClick() { async onClick() {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
let res = await fetch("/api/user/login?type=username&username=" + this.username_input.value, { let res = await fetch(
method: "POST" "/api/user/login?type=username&username=" +
}).then(e => { this.username_input.value,
if (e.status !== 200) throw new Error(e.statusText) {
return e.json() method: "POST",
}).then(data => { }
)
.then((e) => {
if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) { if (data.error) {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error));
} }
return data; return data;
}) });
let salt = res.salt; let salt = res.salt;
this.props.onNext(this.username_input.value, salt); this.props.onNext(this.username_input.value, salt);
} catch (err) { } catch (err) {
this.setState({ this.setState({
error: err.message error: err.message,
}); });
} }
this.setState({ loading: false }); this.setState({ loading: false });
} }
render() { render() {
if (this.state.loading) return <Loader /> if (this.state.loading) return <Loader />;
return <div> return (
<div>
<div class="floating group"> <div class="floating group">
<input onKeyDown={e => { <input
onKeyDown={(e) => {
let k = e.keyCode | e.which; let k = e.keyCode | e.which;
if (k === 13) this.onClick(); if (k === 13) this.onClick();
this.setState({ error: undefined }) 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} /> }}
type="text"
value={
this.username_input
? this.username_input.value
: this.props.username
}
autofocus
ref={(elm) => (elm ? (this.username_input = elm) : undefined)}
/>
<span class="highlight"></span> <span class="highlight"></span>
<span class="bar"></span> <span class="bar"></span>
<label>Username or Email</label> <label>Username or Email</label>
{this.state.error ? <div class="error"> {this.state.error}</div> : undefined} {this.state.error ? (
<div class="error"> {this.state.error}</div>
) : undefined}
</div> </div>
<button type="button" class="mdc-button mdc-button--raised spanned-btn" onClick={() => this.onClick()}>Next</button> <button
type="button"
class="mdc-button mdc-button--raised spanned-btn"
onClick={() => this.onClick()}
>
Next
</button>
</div> </div>
);
} }
} }
@ -70,7 +98,7 @@ enum TFATypes {
OTC, OTC,
BACKUP_CODE, BACKUP_CODE,
YUBI_KEY, YUBI_KEY,
APP_ALLOW APP_ALLOW,
} }
interface TwoFactors { interface TwoFactors {
@ -79,38 +107,50 @@ interface TwoFactors {
type: TFATypes; type: TFATypes;
} }
class Password extends Component<{ username: string, salt: string, onNext: (login: Token, special: Token, tfa: TwoFactors[]) => void }, { error: string, loading: boolean }> { class Password extends Component<
{
username: string;
salt: string;
onNext: (login: Token, special: Token, tfa: TwoFactors[]) => void;
},
{ error: string; loading: boolean }
> {
password_input: HTMLInputElement; password_input: HTMLInputElement;
constructor() { constructor() {
super(); super();
this.state = { error: undefined, loading: false } this.state = { error: undefined, loading: false };
} }
async onClick() { async onClick() {
this.setState({ this.setState({
loading: true loading: true,
}); });
try { try {
let pw = sha(this.props.salt + this.password_input.value); let pw = sha(this.props.salt + this.password_input.value);
let { login, special, tfa } = await fetch("/api/user/login?type=password", { let { login, special, tfa } = await fetch(
"/api/user/login?type=password",
{
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
username: this.props.username, username: this.props.username,
password: pw password: pw,
}), }),
headers: { headers: {
'content-type': 'application/json' "content-type": "application/json",
}, },
}).then(e => { }
if (e.status !== 200) throw new Error(e.statusText) )
return e.json() .then((e) => {
}).then(data => { if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) { if (data.error) {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error));
} }
return data; return data;
}) });
this.props.onNext(login, special, tfa); this.props.onNext(login, special, tfa);
} catch (err) { } catch (err) {
this.setState({ error: err.messagae }); this.setState({ error: err.messagae });
@ -119,65 +159,85 @@ class Password extends Component<{ username: string, salt: string, onNext: (logi
} }
render() { render() {
if (this.state.loading) return <Loader /> if (this.state.loading) return <Loader />;
return <div> return (
<div class="floating group" > <div>
<input onKeyDown={e => { <div class="floating group">
<input
onKeyDown={(e) => {
let k = e.keyCode | e.which; let k = e.keyCode | e.which;
if (k === 13) this.onClick(); if (k === 13) this.onClick();
this.setState({ error: undefined }) this.setState({ error: undefined });
}} type="password" ref={(elm: HTMLInputElement) => { }}
type="password"
ref={(elm: HTMLInputElement) => {
if (elm) { if (elm) {
this.password_input = elm this.password_input = elm;
setTimeout(() => elm.focus(), 200) setTimeout(() => elm.focus(), 200);
// elm.focus(); // elm.focus();
} }
} }}
} /> />
<span class="highlight"></span> <span class="highlight"></span>
<span class="bar"></span> <span class="bar"></span>
<label>Password</label> <label>Password</label>
{this.state.error ? <div class="error"> {this.state.error}</div> : undefined} {this.state.error ? (
<div class="error"> {this.state.error}</div>
) : undefined}
</div> </div>
<button type="button" class="mdc-button mdc-button--raised spanned-btn" onClick={() => this.onClick()}>Login</button> <button
type="button"
class="mdc-button mdc-button--raised spanned-btn"
onClick={() => this.onClick()}
>
Login
</button>
</div> </div>
);
} }
} }
class TwoFactor extends Component<{ twofactors: TwoFactors[], next: (id: string, type: TFATypes) => void }, {}> { class TwoFactor extends Component<
{ twofactors: TwoFactors[]; next: (id: string, type: TFATypes) => void },
{}
> {
render() { render() {
let tfs = this.props.twofactors.map(fac => { let tfs = this.props.twofactors.map((fac) => {
let name: string; let name: string;
switch (fac.type) { switch (fac.type) {
case TFATypes.OTC: case TFATypes.OTC:
name = "Authenticator" name = "Authenticator";
break; break;
case TFATypes.BACKUP_CODE: case TFATypes.BACKUP_CODE:
name = "Backup code"; name = "Backup code";
break; break;
case TFATypes.APP_ALLOW: case TFATypes.APP_ALLOW:
name = "Use App: %s" name = "Use App: %s";
break; break;
case TFATypes.YUBI_KEY: case TFATypes.YUBI_KEY:
name = "Use Yubikey: %s" name = "Use Yubikey: %s";
break; break;
} }
name = name.replace("%s", fac.name ? fac.name : ""); name = name.replace("%s", fac.name ? fac.name : "");
return <li onClick={() => { return (
console.log("Click on Solution") <li
this.props.next(fac.id, fac.type) onClick={() => {
}}> console.log("Click on Solution");
this.props.next(fac.id, fac.type);
}}
>
{name} {name}
</li> </li>
}) );
return <div> });
return (
<div>
<h1>Select one</h1> <h1>Select one</h1>
<ul> <ul>{tfs}</ul>
{tfs}
</ul>
</div> </div>
);
} }
} }
@ -191,7 +251,7 @@ enum Page {
username, username,
password, password,
twofactor, twofactor,
yubikey yubikey,
} }
interface Token { interface Token {
@ -199,54 +259,80 @@ interface Token {
expires: string; expires: string;
} }
async function apiRequest(endpoint: string, method: "GET" | "POST" | "DELETE" | "PUT" = "GET", body: string = undefined) { async function apiRequest(
endpoint: string,
method: "GET" | "POST" | "DELETE" | "PUT" = "GET",
body: string = undefined
) {
return fetch(endpoint, { return fetch(endpoint, {
method, method,
body, body,
credentials: "same-origin", credentials: "same-origin",
headers: { headers: {
"content-type": "application/json" "content-type": "application/json",
} },
}).then(e => { })
if (e.status !== 200) throw new Error(e.statusText) .then((e) => {
return e.json() if (e.status !== 200) throw new Error(e.statusText);
}).then(data => { return e.json();
})
.then((data) => {
if (data.error) { if (data.error) {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error));
} }
return data; return data;
}) });
} }
class App extends Component<
class App extends Component<{}, { page: Page, username: string, salt: string, twofactor: TwoFactors[], twofactor_id: string }> { {},
{
page: Page;
username: string;
salt: string;
twofactor: TwoFactors[];
twofactor_id: string;
}
> {
login: Token; login: Token;
special: Token; special: Token;
constructor() { constructor() {
super(); super();
this.state = { page: Page.username, username: getCookie("username"), salt: undefined, twofactor: [], twofactor_id: null } this.state = {
page: Page.username,
username: getCookie("username"),
salt: undefined,
twofactor: [],
twofactor_id: null,
};
} }
setCookies() { setCookies() {
setCookie("login", this.login.token, new Date(this.login.expires).toUTCString()); setCookie(
setCookie("special", this.special.token, new Date(this.special.expires).toUTCString()); "login",
this.login.token,
new Date(this.login.expires).toUTCString()
);
setCookie(
"special",
this.special.token,
new Date(this.special.expires).toUTCString()
);
} }
finish() { finish() {
this.setCookies(); this.setCookies();
let d = new Date() let d = new Date();
d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000)); //Keep the username 30 days d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
setCookie("username", this.state.username, d.toUTCString()); setCookie("username", this.state.username, d.toUTCString());
let url = new URL(window.location.href); let url = new URL(window.location.href);
let state = url.searchParams.get("state") let state = url.searchParams.get("state");
let red = "/" let red = "/";
if (state) { if (state) {
let base64 = url.searchParams.get("base64") let base64 = url.searchParams.get("base64");
if (base64) if (base64) red = atob(state);
red = atob(state) else red = state;
else
red = state
} }
window.location.href = red; window.location.href = red;
} }
@ -255,13 +341,22 @@ class App extends Component<{}, { page: Page, username: string, salt: string, tw
let cont; let cont;
switch (this.state.page) { switch (this.state.page) {
case Page.username: case Page.username:
cont = <Username username={this.state.username} onNext={(username, salt) => { cont = (
this.setState({ username, salt, page: Page.password }) <Username
username={this.state.username}
onNext={(username, salt) => {
this.setState({ username, salt, page: Page.password });
localStorage.setItem("username", username); localStorage.setItem("username", username);
}} /> }}
/>
);
break; break;
case Page.password: case Page.password:
cont = <Password username={this.state.username} salt={this.state.salt} onNext={(login, special, twofactor) => { cont = (
<Password
username={this.state.username}
salt={this.state.salt}
onNext={(login, special, twofactor) => {
this.login = login; this.login = login;
this.special = special; this.special = special;
this.setCookies(); this.setCookies();
@ -271,23 +366,42 @@ class App extends Component<{}, { page: Page, username: string, salt: string, tw
} else { } else {
this.setState({ twofactor, page: Page.twofactor }); this.setState({ twofactor, page: Page.twofactor });
} }
}} /> }}
/>
);
break; break;
case Page.twofactor: case Page.twofactor:
cont = <TwoFactor twofactors={this.state.twofactor} next={async (id, type) => { cont = (
<TwoFactor
twofactors={this.state.twofactor}
next={async (id, type) => {
if (type === TFATypes.YUBI_KEY) { if (type === TFATypes.YUBI_KEY) {
let { request } = await apiRequest("/api/user/twofactor/yubikey", "GET"); let { request } = await apiRequest(
"/api/user/twofactor/yubikey",
"GET"
);
console.log(request); console.log(request);
(window as any).u2f.sign(request.appId, [request.challenge], [request], async (response) => { (window as any).u2f.sign(
let res = await apiRequest("/api/user/twofactor/yubikey", "PUT", JSON.stringify({ response })); request.appId,
[request.challenge],
[request],
async (response) => {
let res = await apiRequest(
"/api/user/twofactor/yubikey",
"PUT",
JSON.stringify({ response })
);
if (res.success) { if (res.success) {
this.login.expires = res.login_exp; this.login.expires = res.login_exp;
this.special.expires = res.special_exp; this.special.expires = res.special_exp;
this.finish(); this.finish();
} }
})
} }
}} /> );
}
}}
/>
);
break; break;
// case Page.yubikey: // case Page.yubikey:
// cont = <TFA_YubiKey id={this.state.twofactor_id} login={this.login} special={this.special} next={(login, special) => { // cont = <TFA_YubiKey id={this.state.twofactor_id} login={this.login} special={this.special} next={(login, special) => {
@ -297,20 +411,24 @@ class App extends Component<{}, { page: Page, username: string, salt: string, tw
// }} /> // }} />
// break; // break;
} }
return <div> return (
<div>
<header> <header>
<h1>Login</h1> <h1>Login</h1>
</header> </header>
<form action="JavaScript:void(0)"> <form action="JavaScript:void(0)">{cont}</form>
{cont}
</form>
<footer> <footer>
<p>Powered by {appname}</p> <p>Powered by {appname}</p>
</footer> </footer>
</div> </div>
);
} }
} }
document.addEventListener('DOMContentLoaded', function () { document.addEventListener(
render(<App />, document.body.querySelector("#content")) "DOMContentLoaded",
}, false) function () {
render(<App />, document.body.querySelector("#content"));
},
false
);

View File

@ -12,13 +12,14 @@
/** /**
* @fileoverview The U2F api. * @fileoverview The U2F api.
*/ */
'use strict'; "use strict";
// NECESSARY CHANGE: wrap the whole file in a closure // NECESSARY CHANGE: wrap the whole file in a closure
(function (){ (function () {
// NECESSARY CHANGE: detect UA to avoid clobbering other browser's U2F API. // NECESSARY CHANGE: detect UA to avoid clobbering other browser's U2F API.
var isChrome = 'chrome' in window && window.navigator.userAgent.indexOf('Edge') < 0; var isChrome =
if ('u2f' in window || !isChrome) { "chrome" in window && window.navigator.userAgent.indexOf("Edge") < 0;
if ("u2f" in window || !isChrome) {
return; return;
} }
@ -27,7 +28,7 @@
* @type {Object} * @type {Object}
*/ */
// NECESSARY CHANGE: define the window.u2f API. // NECESSARY CHANGE: define the window.u2f API.
var u2f = window.u2f = {}; var u2f = (window.u2f = {});
/** /**
* FIDO U2F Javascript API Version * FIDO U2F Javascript API Version
@ -42,43 +43,40 @@
// The Chrome packaged app extension ID. // The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses // 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. // the package Chrome app and does not require installing the U2F Chrome extension.
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; u2f.EXTENSION_ID = "kmendfapggjehodndflmmgagdbamhnfd";
// The U2F Chrome extension ID. // The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses // Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate. // the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
/** /**
* Message types for messsages to/from the extension * Message types for messsages to/from the extension
* @const * @const
* @enum {string} * @enum {string}
*/ */
u2f.MessageTypes = { u2f.MessageTypes = {
'U2F_REGISTER_REQUEST': 'u2f_register_request', U2F_REGISTER_REQUEST: "u2f_register_request",
'U2F_REGISTER_RESPONSE': 'u2f_register_response', U2F_REGISTER_RESPONSE: "u2f_register_response",
'U2F_SIGN_REQUEST': 'u2f_sign_request', U2F_SIGN_REQUEST: "u2f_sign_request",
'U2F_SIGN_RESPONSE': 'u2f_sign_response', U2F_SIGN_RESPONSE: "u2f_sign_response",
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', U2F_GET_API_VERSION_REQUEST: "u2f_get_api_version_request",
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' U2F_GET_API_VERSION_RESPONSE: "u2f_get_api_version_response",
}; };
/** /**
* Response status codes * Response status codes
* @const * @const
* @enum {number} * @enum {number}
*/ */
u2f.ErrorCodes = { u2f.ErrorCodes = {
'OK': 0, OK: 0,
'OTHER_ERROR': 1, OTHER_ERROR: 1,
'BAD_REQUEST': 2, BAD_REQUEST: 2,
'CONFIGURATION_UNSUPPORTED': 3, CONFIGURATION_UNSUPPORTED: 3,
'DEVICE_INELIGIBLE': 4, DEVICE_INELIGIBLE: 4,
'TIMEOUT': 5 TIMEOUT: 5,
}; };
/** /**
* A message for registration requests * A message for registration requests
* @typedef {{ * @typedef {{
@ -90,7 +88,6 @@
*/ */
u2f.U2fRequest; u2f.U2fRequest;
/** /**
* A message for registration responses * A message for registration responses
* @typedef {{ * @typedef {{
@ -101,7 +98,6 @@
*/ */
u2f.U2fResponse; u2f.U2fResponse;
/** /**
* An error object for responses * An error object for responses
* @typedef {{ * @typedef {{
@ -117,7 +113,6 @@
*/ */
u2f.Transport; u2f.Transport;
/** /**
* Data object for a single sign request. * Data object for a single sign request.
* @typedef {Array<u2f.Transport>} * @typedef {Array<u2f.Transport>}
@ -135,7 +130,6 @@
*/ */
u2f.SignRequest; u2f.SignRequest;
/** /**
* Data object for a sign response. * Data object for a sign response.
* @typedef {{ * @typedef {{
@ -146,7 +140,6 @@
*/ */
u2f.SignResponse; u2f.SignResponse;
/** /**
* Data object for a registration request. * Data object for a registration request.
* @typedef {{ * @typedef {{
@ -156,7 +149,6 @@
*/ */
u2f.RegisterRequest; u2f.RegisterRequest;
/** /**
* Data object for a registration response. * Data object for a registration response.
* @typedef {{ * @typedef {{
@ -168,7 +160,6 @@
*/ */
u2f.RegisterResponse; u2f.RegisterResponse;
/** /**
* Data object for a registered key. * Data object for a registered key.
* @typedef {{ * @typedef {{
@ -180,7 +171,6 @@
*/ */
u2f.RegisteredKey; u2f.RegisteredKey;
/** /**
* Data object for a get API register response. * Data object for a get API register response.
* @typedef {{ * @typedef {{
@ -189,7 +179,6 @@
*/ */
u2f.GetJsApiVersionResponse; u2f.GetJsApiVersionResponse;
//Low level MessagePort API support //Low level MessagePort API support
/** /**
@ -197,16 +186,16 @@
* available mechanisms. * available mechanisms.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/ */
u2f.getMessagePort = function(callback) { u2f.getMessagePort = function (callback) {
if (typeof chrome != 'undefined' && chrome.runtime) { if (typeof chrome != "undefined" && chrome.runtime) {
// The actual message here does not matter, but we need to get a reply // 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 // for the callback to run. Thus, send an empty signature request
// in order to get a failure response. // in order to get a failure response.
var msg = { var msg = {
type: u2f.MessageTypes.U2F_SIGN_REQUEST, type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: [] signRequests: [],
}; };
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () {
if (!chrome.runtime.lastError) { if (!chrome.runtime.lastError) {
// We are on a whitelisted origin and can talk directly // We are on a whitelisted origin and can talk directly
// with the extension. // with the extension.
@ -232,17 +221,18 @@
* Detect chrome running on android based on the browser's useragent. * Detect chrome running on android based on the browser's useragent.
* @private * @private
*/ */
u2f.isAndroidChrome_ = function() { u2f.isAndroidChrome_ = function () {
var userAgent = navigator.userAgent; var userAgent = navigator.userAgent;
return userAgent.indexOf('Chrome') != -1 && return (
userAgent.indexOf('Android') != -1; userAgent.indexOf("Chrome") != -1 && userAgent.indexOf("Android") != -1
);
}; };
/** /**
* Detect chrome running on iOS based on the browser's platform. * Detect chrome running on iOS based on the browser's platform.
* @private * @private
*/ */
u2f.isIosChrome_ = function() { u2f.isIosChrome_ = function () {
return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
}; };
@ -251,10 +241,11 @@
* @param {function(u2f.WrappedChromeRuntimePort_)} callback * @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private * @private
*/ */
u2f.getChromeRuntimePort_ = function(callback) { u2f.getChromeRuntimePort_ = function (callback) {
var port = chrome.runtime.connect(u2f.EXTENSION_ID, var port = chrome.runtime.connect(u2f.EXTENSION_ID, {
{'includeTlsChannelId': true}); includeTlsChannelId: true,
setTimeout(function() { });
setTimeout(function () {
callback(new u2f.WrappedChromeRuntimePort_(port)); callback(new u2f.WrappedChromeRuntimePort_(port));
}, 0); }, 0);
}; };
@ -264,8 +255,8 @@
* @param {function(u2f.WrappedAuthenticatorPort_)} callback * @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private * @private
*/ */
u2f.getAuthenticatorPort_ = function(callback) { u2f.getAuthenticatorPort_ = function (callback) {
setTimeout(function() { setTimeout(function () {
callback(new u2f.WrappedAuthenticatorPort_()); callback(new u2f.WrappedAuthenticatorPort_());
}, 0); }, 0);
}; };
@ -275,8 +266,8 @@
* @param {function(u2f.WrappedIosPort_)} callback * @param {function(u2f.WrappedIosPort_)} callback
* @private * @private
*/ */
u2f.getIosPort_ = function(callback) { u2f.getIosPort_ = function (callback) {
setTimeout(function() { setTimeout(function () {
callback(new u2f.WrappedIosPort_()); callback(new u2f.WrappedIosPort_());
}, 0); }, 0);
}; };
@ -287,7 +278,7 @@
* @constructor * @constructor
* @private * @private
*/ */
u2f.WrappedChromeRuntimePort_ = function(port) { u2f.WrappedChromeRuntimePort_ = function (port) {
this.port_ = port; this.port_ = port;
}; };
@ -298,8 +289,13 @@
* @param {number} reqId * @param {number} reqId
* @return {Object} * @return {Object}
*/ */
u2f.formatSignRequest_ = u2f.formatSignRequest_ = function (
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { appId,
challenge,
registeredKeys,
timeoutSeconds,
reqId
) {
if (js_api_version === undefined || js_api_version < 1.1) { if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API // Adapt request to the 1.0 JS API
var signRequests = []; var signRequests = [];
@ -308,14 +304,14 @@
version: registeredKeys[i].version, version: registeredKeys[i].version,
challenge: challenge, challenge: challenge,
keyHandle: registeredKeys[i].keyHandle, keyHandle: registeredKeys[i].keyHandle,
appId: appId appId: appId,
}; };
} }
return { return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST, type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: signRequests, signRequests: signRequests,
timeoutSeconds: timeoutSeconds, timeoutSeconds: timeoutSeconds,
requestId: reqId requestId: reqId,
}; };
} }
// JS 1.1 API // JS 1.1 API
@ -325,7 +321,7 @@
challenge: challenge, challenge: challenge,
registeredKeys: registeredKeys, registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds, timeoutSeconds: timeoutSeconds,
requestId: reqId requestId: reqId,
}; };
}; };
@ -337,8 +333,13 @@
* @param {number} reqId * @param {number} reqId
* @return {Object} * @return {Object}
*/ */
u2f.formatRegisterRequest_ = u2f.formatRegisterRequest_ = function (
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { appId,
registeredKeys,
registerRequests,
timeoutSeconds,
reqId
) {
if (js_api_version === undefined || js_api_version < 1.1) { if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API // Adapt request to the 1.0 JS API
for (var i = 0; i < registerRequests.length; i++) { for (var i = 0; i < registerRequests.length; i++) {
@ -350,7 +351,7 @@
version: registeredKeys[i].version, version: registeredKeys[i].version,
challenge: registerRequests[0], challenge: registerRequests[0],
keyHandle: registeredKeys[i].keyHandle, keyHandle: registeredKeys[i].keyHandle,
appId: appId appId: appId,
}; };
} }
return { return {
@ -358,7 +359,7 @@
signRequests: signRequests, signRequests: signRequests,
registerRequests: registerRequests, registerRequests: registerRequests,
timeoutSeconds: timeoutSeconds, timeoutSeconds: timeoutSeconds,
requestId: reqId requestId: reqId,
}; };
} }
// JS 1.1 API // JS 1.1 API
@ -368,36 +369,36 @@
registerRequests: registerRequests, registerRequests: registerRequests,
registeredKeys: registeredKeys, registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds, timeoutSeconds: timeoutSeconds,
requestId: reqId requestId: reqId,
}; };
}; };
/** /**
* Posts a message on the underlying channel. * Posts a message on the underlying channel.
* @param {Object} message * @param {Object} message
*/ */
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) {
this.port_.postMessage(message); this.port_.postMessage(message);
}; };
/** /**
* Emulates the HTML 5 addEventListener interface. Works only for the * Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
* @param {string} eventName * @param {string} eventName
* @param {function({data: Object})} handler * @param {function({data: Object})} handler
*/ */
u2f.WrappedChromeRuntimePort_.prototype.addEventListener = u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function (
function(eventName, handler) { eventName,
handler
) {
var name = eventName.toLowerCase(); var name = eventName.toLowerCase();
if (name == 'message' || name == 'onmessage') { if (name == "message" || name == "onmessage") {
this.port_.onMessage.addListener(function(message) { this.port_.onMessage.addListener(function (message) {
// Emulate a minimal MessageEvent object // Emulate a minimal MessageEvent object
handler({'data': message}); handler({ data: message });
}); });
} else { } else {
console.error('WrappedChromeRuntimePort only supports onMessage'); console.error("WrappedChromeRuntimePort only supports onMessage");
} }
}; };
@ -406,20 +407,21 @@
* @constructor * @constructor
* @private * @private
*/ */
u2f.WrappedAuthenticatorPort_ = function() { u2f.WrappedAuthenticatorPort_ = function () {
this.requestId_ = -1; this.requestId_ = -1;
this.requestObject_ = null; this.requestObject_ = null;
} };
/** /**
* Launch the Authenticator intent. * Launch the Authenticator intent.
* @param {Object} message * @param {Object} message
*/ */
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) {
var intentUrl = var intentUrl =
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
';S.request=' + encodeURIComponent(JSON.stringify(message)) + ";S.request=" +
';end'; encodeURIComponent(JSON.stringify(message)) +
";end";
document.location = intentUrl; document.location = intentUrl;
}; };
@ -427,26 +429,31 @@
* Tells what type of port this is. * Tells what type of port this is.
* @return {String} port type * @return {String} port type
*/ */
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () {
return "WrappedAuthenticatorPort_"; return "WrappedAuthenticatorPort_";
}; };
/** /**
* Emulates the HTML 5 addEventListener interface. * Emulates the HTML 5 addEventListener interface.
* @param {string} eventName * @param {string} eventName
* @param {function({data: Object})} handler * @param {function({data: Object})} handler
*/ */
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function (
eventName,
handler
) {
var name = eventName.toLowerCase(); var name = eventName.toLowerCase();
if (name == 'message') { if (name == "message") {
var self = this; var self = this;
/* Register a callback to that executes when /* Register a callback to that executes when
* chrome injects the response. */ * chrome injects the response. */
window.addEventListener( window.addEventListener(
'message', self.onRequestUpdate_.bind(self, handler), false); "message",
self.onRequestUpdate_.bind(self, handler),
false
);
} else { } else {
console.error('WrappedAuthenticatorPort only supports message'); console.error("WrappedAuthenticatorPort only supports message");
} }
}; };
@ -455,19 +462,22 @@
* @param function({data: Object}) callback * @param function({data: Object}) callback
* @param {Object} message message Object * @param {Object} message message Object
*/ */
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function (
function(callback, message) { callback,
message
) {
var messageObject = JSON.parse(message.data); var messageObject = JSON.parse(message.data);
var intentUrl = messageObject['intentURL']; var intentUrl = messageObject["intentURL"];
var errorCode = messageObject['errorCode']; var errorCode = messageObject["errorCode"];
var responseObject = null; var responseObject = null;
if (messageObject.hasOwnProperty('data')) { if (messageObject.hasOwnProperty("data")) {
responseObject = /** @type {Object} */ ( responseObject = /** @type {Object} */ (JSON.parse(
JSON.parse(messageObject['data'])); messageObject["data"]
));
} }
callback({'data': responseObject}); callback({ data: responseObject });
}; };
/** /**
@ -476,20 +486,20 @@
* @private * @private
*/ */
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; "intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE";
/** /**
* Wrap the iOS client app with a MessagePort interface. * Wrap the iOS client app with a MessagePort interface.
* @constructor * @constructor
* @private * @private
*/ */
u2f.WrappedIosPort_ = function() {}; u2f.WrappedIosPort_ = function () {};
/** /**
* Launch the iOS client app request * Launch the iOS client app request
* @param {Object} message * @param {Object} message
*/ */
u2f.WrappedIosPort_.prototype.postMessage = function(message) { u2f.WrappedIosPort_.prototype.postMessage = function (message) {
var str = JSON.stringify(message); var str = JSON.stringify(message);
var url = "u2f://auth?" + encodeURI(str); var url = "u2f://auth?" + encodeURI(str);
location.replace(url); location.replace(url);
@ -499,7 +509,7 @@
* Tells what type of port this is. * Tells what type of port this is.
* @return {String} port type * @return {String} port type
*/ */
u2f.WrappedIosPort_.prototype.getPortType = function() { u2f.WrappedIosPort_.prototype.getPortType = function () {
return "WrappedIosPort_"; return "WrappedIosPort_";
}; };
@ -508,10 +518,13 @@
* @param {string} eventName * @param {string} eventName
* @param {function({data: Object})} handler * @param {function({data: Object})} handler
*/ */
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { u2f.WrappedIosPort_.prototype.addEventListener = function (
eventName,
handler
) {
var name = eventName.toLowerCase(); var name = eventName.toLowerCase();
if (name !== 'message') { if (name !== "message") {
console.error('WrappedIosPort only supports message'); console.error("WrappedIosPort only supports message");
} }
}; };
@ -520,33 +533,34 @@
* @param {function(MessagePort)} callback * @param {function(MessagePort)} callback
* @private * @private
*/ */
u2f.getIframePort_ = function(callback) { u2f.getIframePort_ = function (callback) {
// Create the iframe // Create the iframe
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; var iframeOrigin = "chrome-extension://" + u2f.EXTENSION_ID;
var iframe = document.createElement('iframe'); var iframe = document.createElement("iframe");
iframe.src = iframeOrigin + '/u2f-comms.html'; iframe.src = iframeOrigin + "/u2f-comms.html";
iframe.setAttribute('style', 'display:none'); iframe.setAttribute("style", "display:none");
document.body.appendChild(iframe); document.body.appendChild(iframe);
var channel = new MessageChannel(); var channel = new MessageChannel();
var ready = function(message) { var ready = function (message) {
if (message.data == 'ready') { if (message.data == "ready") {
channel.port1.removeEventListener('message', ready); channel.port1.removeEventListener("message", ready);
callback(channel.port1); callback(channel.port1);
} else { } else {
console.error('First event on iframe port was not "ready"'); console.error('First event on iframe port was not "ready"');
} }
}; };
channel.port1.addEventListener('message', ready); channel.port1.addEventListener("message", ready);
channel.port1.start(); channel.port1.start();
iframe.addEventListener('load', function() { iframe.addEventListener("load", function () {
// Deliver the port to the iframe and initialize // Deliver the port to the iframe and initialize
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); iframe.contentWindow.postMessage("init", iframeOrigin, [
channel.port2,
]);
}); });
}; };
//High-level JS API //High-level JS API
/** /**
@ -589,15 +603,17 @@
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private * @private
*/ */
u2f.getPortSingleton_ = function(callback) { u2f.getPortSingleton_ = function (callback) {
if (u2f.port_) { if (u2f.port_) {
callback(u2f.port_); callback(u2f.port_);
} else { } else {
if (u2f.waitingForPort_.length == 0) { if (u2f.waitingForPort_.length == 0) {
u2f.getMessagePort(function(port) { u2f.getMessagePort(function (port) {
u2f.port_ = port; u2f.port_ = port;
u2f.port_.addEventListener('message', u2f.port_.addEventListener(
/** @type {function(Event)} */ (u2f.responseHandler_)); "message",
/** @type {function(Event)} */ (u2f.responseHandler_)
);
// Careful, here be async callbacks. Maybe. // Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length) while (u2f.waitingForPort_.length)
@ -613,16 +629,16 @@
* @param {MessageEvent.<u2f.Response>} message * @param {MessageEvent.<u2f.Response>} message
* @private * @private
*/ */
u2f.responseHandler_ = function(message) { u2f.responseHandler_ = function (message) {
var response = message.data; var response = message.data;
var reqId = response['requestId']; var reqId = response["requestId"];
if (!reqId || !u2f.callbackMap_[reqId]) { if (!reqId || !u2f.callbackMap_[reqId]) {
console.error('Unknown or missing requestId in response.'); console.error("Unknown or missing requestId in response.");
return; return;
} }
var cb = u2f.callbackMap_[reqId]; var cb = u2f.callbackMap_[reqId];
delete u2f.callbackMap_[reqId]; delete u2f.callbackMap_[reqId];
cb(response['responseData']); cb(response["responseData"]);
}; };
/** /**
@ -636,18 +652,38 @@
* @param {function((u2f.Error|u2f.SignResponse))} callback * @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds * @param {number=} opt_timeoutSeconds
*/ */
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { u2f.sign = function (
appId,
challenge,
registeredKeys,
callback,
opt_timeoutSeconds
) {
if (js_api_version === undefined) { if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual sign request. // Send a message to get the extension to JS API version, then send the actual sign request.
u2f.getApiVersion( u2f.getApiVersion(function (response) {
function (response) { js_api_version =
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; response["js_api_version"] === undefined
? 0
: response["js_api_version"];
console.log("Extension JS API Version: ", js_api_version); console.log("Extension JS API Version: ", js_api_version);
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); u2f.sendSignRequest(
appId,
challenge,
registeredKeys,
callback,
opt_timeoutSeconds
);
}); });
} else { } else {
// We know the JS API version. Send the actual sign request in the supported API version. // We know the JS API version. Send the actual sign request in the supported API version.
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); u2f.sendSignRequest(
appId,
challenge,
registeredKeys,
callback,
opt_timeoutSeconds
);
} }
}; };
@ -659,13 +695,27 @@
* @param {function((u2f.Error|u2f.SignResponse))} callback * @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds * @param {number=} opt_timeoutSeconds
*/ */
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { u2f.sendSignRequest = function (
u2f.getPortSingleton_(function(port) { appId,
challenge,
registeredKeys,
callback,
opt_timeoutSeconds
) {
u2f.getPortSingleton_(function (port) {
var reqId = ++u2f.reqCounter_; var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback; u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? var timeoutSeconds =
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); typeof opt_timeoutSeconds !== "undefined"
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); ? opt_timeoutSeconds
: u2f.EXTENSION_TIMEOUT_SEC;
var req = u2f.formatSignRequest_(
appId,
challenge,
registeredKeys,
timeoutSeconds,
reqId
);
port.postMessage(req); port.postMessage(req);
}); });
}; };
@ -682,20 +732,38 @@
* @param {function((u2f.Error|u2f.RegisterResponse))} callback * @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds * @param {number=} opt_timeoutSeconds
*/ */
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { u2f.register = function (
appId,
registerRequests,
registeredKeys,
callback,
opt_timeoutSeconds
) {
if (js_api_version === undefined) { if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual register request. // Send a message to get the extension to JS API version, then send the actual register request.
u2f.getApiVersion( u2f.getApiVersion(function (response) {
function (response) { js_api_version =
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; response["js_api_version"] === undefined
? 0
: response["js_api_version"];
console.log("Extension JS API Version: ", js_api_version); console.log("Extension JS API Version: ", js_api_version);
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, u2f.sendRegisterRequest(
callback, opt_timeoutSeconds); appId,
registerRequests,
registeredKeys,
callback,
opt_timeoutSeconds
);
}); });
} else { } else {
// We know the JS API version. Send the actual register request in the supported API version. // We know the JS API version. Send the actual register request in the supported API version.
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, u2f.sendRegisterRequest(
callback, opt_timeoutSeconds); appId,
registerRequests,
registeredKeys,
callback,
opt_timeoutSeconds
);
} }
}; };
@ -708,19 +776,31 @@
* @param {function((u2f.Error|u2f.RegisterResponse))} callback * @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds * @param {number=} opt_timeoutSeconds
*/ */
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { u2f.sendRegisterRequest = function (
u2f.getPortSingleton_(function(port) { appId,
registerRequests,
registeredKeys,
callback,
opt_timeoutSeconds
) {
u2f.getPortSingleton_(function (port) {
var reqId = ++u2f.reqCounter_; var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback; u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? var timeoutSeconds =
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); typeof opt_timeoutSeconds !== "undefined"
? opt_timeoutSeconds
: u2f.EXTENSION_TIMEOUT_SEC;
var req = u2f.formatRegisterRequest_( var req = u2f.formatRegisterRequest_(
appId, registeredKeys, registerRequests, timeoutSeconds, reqId); appId,
registeredKeys,
registerRequests,
timeoutSeconds,
reqId
);
port.postMessage(req); port.postMessage(req);
}); });
}; };
/** /**
* Dispatches a message to the extension to find out the supported * Dispatches a message to the extension to find out the supported
* JS API version. * JS API version.
@ -729,15 +809,15 @@
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds * @param {number=} opt_timeoutSeconds
*/ */
u2f.getApiVersion = function(callback, opt_timeoutSeconds) { u2f.getApiVersion = function (callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) { u2f.getPortSingleton_(function (port) {
// If we are using Android Google Authenticator or iOS client app, // If we are using Android Google Authenticator or iOS client app,
// do not fire an intent to ask which JS API version to use. // do not fire an intent to ask which JS API version to use.
if (port.getPortType) { if (port.getPortType) {
var apiVersion; var apiVersion;
switch (port.getPortType()) { switch (port.getPortType()) {
case 'WrappedIosPort_': case "WrappedIosPort_":
case 'WrappedAuthenticatorPort_': case "WrappedAuthenticatorPort_":
apiVersion = 1.1; apiVersion = 1.1;
break; break;
@ -745,16 +825,18 @@
apiVersion = 0; apiVersion = 0;
break; break;
} }
callback({ 'js_api_version': apiVersion }); callback({ js_api_version: apiVersion });
return; return;
} }
var reqId = ++u2f.reqCounter_; var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback; u2f.callbackMap_[reqId] = callback;
var req = { var req = {
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? timeoutSeconds:
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), typeof opt_timeoutSeconds !== "undefined"
requestId: reqId ? opt_timeoutSeconds
: u2f.EXTENSION_TIMEOUT_SEC,
requestId: reqId,
}; };
port.postMessage(req); port.postMessage(req);
}); });

View File

@ -1 +1 @@
console.log("Hello World") console.log("Hello World");

View File

@ -1,23 +1,25 @@
import "inputs"; import "inputs";
import sha from "sha512"; 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") const regcode = document.getElementById("regcode");
regcode.value = new URL(window.location.href).searchParams.get("regcode") regcode.value = new URL(window.location.href).searchParams.get("regcode");
fireEvent(regcode, "change"); fireEvent(regcode, "change");
function showError(element, message) { function showError(element, message) {
if (typeof element === "string") 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; element.innerText = message;
if (!message) { if (!message) {
if (!element.classList.contains("invisible")) if (!element.classList.contains("invisible"))
element.classList.add("invisible") element.classList.add("invisible");
} else { } else {
element.classList.remove("invisible"); element.classList.remove("invisible");
} }
@ -25,7 +27,8 @@ import fireEvent from "event"
function makeid(length) { function makeid(length) {
var text = ""; var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++) for (var i = 0; i < length; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length)); text += possible.charAt(Math.floor(Math.random() * possible.length));
@ -33,61 +36,61 @@ import fireEvent from "event"
return text; return text;
} }
const username = document.getElementById("username") const username = document.getElementById("username");
const name = document.getElementById("name") const name = document.getElementById("name");
const mail = document.getElementById("mail") const mail = document.getElementById("mail");
const password = document.getElementById("password") const password = document.getElementById("password");
const passwordrep = document.getElementById("passwordrep") const passwordrep = document.getElementById("passwordrep");
const radio_male = document.getElementById("radio-male") const radio_male = document.getElementById("radio-male");
const radio_female = document.getElementById("radio-female") const radio_female = document.getElementById("radio-female");
const radio_other = document.getElementById("radio-other") const radio_other = document.getElementById("radio-other");
const registerButton = document.getElementById("registerbutton") const registerButton = document.getElementById("registerbutton");
registerButton.onclick = () => { registerButton.onclick = () => {
console.log("Register") console.log("Register");
showError("error"); showError("error");
let error = false; let error = false;
if (!regcode.value) { if (!regcode.value) {
showError("err_regcode", translations["noregcode"]) showError("err_regcode", translations["noregcode"]);
error = true; error = true;
} else { } else {
showError("err_regcode") showError("err_regcode");
} }
if (!username.value) { if (!username.value) {
showError("err_username", translations["nousername"]) showError("err_username", translations["nousername"]);
error = true; error = true;
} else { } else {
showError("err_username") showError("err_username");
} }
if (!name.value) { if (!name.value) {
showError("err_name", translations["noname"]) showError("err_name", translations["noname"]);
error = true; error = true;
} else { } else {
showError("err_name") showError("err_name");
} }
if (!mail.value) { if (!mail.value) {
showError("err_mail", translations["nomail"]) showError("err_mail", translations["nomail"]);
error = true; error = true;
} else { } else {
showError("err_mail") showError("err_mail");
} }
if (!password.value) { if (!password.value) {
showError("err_password", translations["nopassword"]) showError("err_password", translations["nopassword"]);
error = true; error = true;
} else { } else {
showError("err_password") showError("err_password");
} }
if (password.value !== passwordrep.value) { if (password.value !== passwordrep.value) {
showError("err_passwordrep", translations["nomatch"]) showError("err_passwordrep", translations["nomatch"]);
error = true; error = true;
} else { } else {
showError("err_passwordrep") showError("err_passwordrep");
} }
if (error) return; if (error) return;
@ -95,14 +98,14 @@ import fireEvent from "event"
let gender; let gender;
if (radio_male.checked) { if (radio_male.checked) {
gender = "male" gender = "male";
} else if (radio_female.checked) { } else if (radio_female.checked) {
gender = "female" gender = "female";
} else { } else {
gender = "other" gender = "other";
} }
let salt = makeid(10) let salt = makeid(10);
//username, password, salt, mail, gender, name, birthday, regcode //username, password, salt, mail, gender, name, birthday, regcode
@ -113,37 +116,44 @@ import fireEvent from "event"
name: name.value, name: name.value,
regcode: regcode.value, regcode: regcode.value,
salt: salt, salt: salt,
password: sha(salt + password.value) password: sha(salt + password.value),
} };
fetch("/api/user/register", { fetch("/api/user/register", {
method: "POST", method: "POST",
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { 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)); .then(async (e) => {
return e.json() if (e.status !== 200)
}).then(data => { return Promise.reject(
new Error((await e.text()) || e.statusText)
);
return e.json();
})
.then((data) => {
if (data.error) { if (data.error) {
if (!Array.isArray(data.error)) return Promise.reject(new Error(data.error)); if (!Array.isArray(data.error))
return Promise.reject(new Error(data.error));
let ce = []; let ce = [];
data.error.forEach(e => { data.error.forEach((e) => {
let ef = document.getElementById("err_" + e.field); let ef = document.getElementById("err_" + e.field);
if (!ef) ce.push(e); if (!ef) ce.push(e);
else { else {
showError(ef, e.message); showError(ef, e.message);
} }
}) });
if (ce.length > 0) { if (ce.length > 0) {
showError("error", ce.join("<br>")); showError("error", ce.join("<br>"));
} }
} else { } else {
window.location.href = "/login"; window.location.href = "/login";
} }
}).catch(e => {
showError("error", e.message);
}) })
} .catch((e) => {
})() showError("error", e.message);
});
};
})();

View File

@ -36,13 +36,14 @@ form {
padding: 3em 2em 2em 2em; padding: 3em 2em 2em 2em;
background: #fafafa; background: #fafafa;
border: 1px solid #ebebeb; 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 { #registerbutton {
width: 100%; width: 100%;
background: $primary; background: $primary;
text-shadow: 1px 1px 0 rgba(39, 110, 204, .5); text-shadow: 1px 1px 0 rgba(39, 110, 204, 0.5);
} }
footer { footer {
@ -52,13 +53,13 @@ footer {
footer p { footer p {
color: #888; color: #888;
font-size: 13px; font-size: 13px;
letter-spacing: .4px; letter-spacing: 0.4px;
} }
footer a { footer a {
color: $primary; color: $primary;
text-decoration: none; text-decoration: none;
transition: all .2s ease; transition: all 0.2s ease;
} }
footer a:hover { footer a:hover {
@ -68,11 +69,11 @@ footer a:hover {
footer img { footer img {
width: 80px; width: 80px;
transition: all .2s ease; transition: all 0.2s ease;
} }
footer img:hover { footer img:hover {
opacity: .83; opacity: 0.83;
} }
footer img:focus, footer img:focus,

View File

@ -1,18 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": ["dom", "es2015", "es6", "es7", "es2018", "esnext"],
"dom",
"es2015",
"es6",
"es7",
"es2018",
"esnext"
],
"jsxFactory": "h", "jsxFactory": "h",
"jsx": "react", "jsx": "react",
"module": "esnext" "module": "esnext"
}, },
"include": [ "include": ["./types.d.ts"]
"./types.d.ts"
]
} }

@ -1 +1 @@
Subproject commit e4d08dcbf93f58ff85f54f7770ae7db490ff14d6 Subproject commit 4191522b24334f20f7dcda811ca43959b02fef7a