19 Commits

Author SHA1 Message Date
1e2bb83447 Add new profile endpoint
Add some logging output for auth failures
2023-04-07 23:01:56 +02:00
0453e461c9 Restructuring the Project
Updating dependencies
2023-04-07 19:54:47 +02:00
532107c479 Change test client definition
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-20 00:16:40 +01:00
39628e6175 Add endpoint for getting config
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-20 00:06:04 +01:00
e814aacb86 Add the new fields to the admin API
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-20 00:04:09 +01:00
bb01f7d62d Adding API Endpoint for featured clients
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-20 00:02:36 +01:00
779e7e1478 Fix last bugs
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-19 18:31:19 +01:00
0e55b154ed Switching to new registry
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-19 18:25:44 +01:00
7c0d5949ab Fix typescript errors and update depencies
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-19 16:15:34 +01:00
05bef4fd49 Add user login check for popup UI
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-05 11:58:38 +01:00
d6b72f4fc7 Updating views_repo
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-04 04:50:05 +01:00
69aa9cb47e update views_repo
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-04 04:43:22 +01:00
dd10cae1cd Improve popup window support.
All checks were successful
continuous-integration/drone/push Build is passing
Switching to new views_repo with new build system
2020-11-03 23:29:56 +01:00
6b4ad81940 Adding a popup authentication option.
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-28 05:11:47 +01:00
2c4c87927d Fix bug with empty scopes
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-07 19:08:46 +02:00
e0ea7275f7 Fix bug with empty scope
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-07 19:01:29 +02:00
51a8609880 Running prettier
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-07 16:16:39 +02:00
77fedd2815 Removing uneccessary log
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-07 16:04:06 +02:00
e51069deab Updating view repo
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-07 16:03:47 +02:00
205 changed files with 22035 additions and 9065 deletions

11
.dockerignore Normal file
View File

@ -0,0 +1,11 @@
node_modules/
Backend/node_modules
Backend/keys
Backend/logs
Backend/lib
Backend/doc
Backend/config.ini
Frontend/build
Frontend/node_modules
FrontendLegacy/node_modules
FrontendLegacy/out

View File

@ -6,6 +6,7 @@ steps:
- name: Build with node
image: node:12
commands:
- npm config set registry https://npm.hibas123.de
- npm install
- npm run install
- npm run build
@ -17,8 +18,11 @@ steps:
password:
from_secret: docker_password
auto_tag: true
repo: hibas123.azurecr.io/authserver
registry: hibas123.azurecr.io
repo: docker.hibas123.de/authserver
registry: docker.hibas123.de
debug: true
when:
branch: master
branch: [master]
event:
exclude:
- pull_request

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ yarn-error\.log
config.ini
.env
doc/
.yarn/cache
.yarn/install-state.gz

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "views_repo"]
path = views_repo
url = ../OpenAuth_views

File diff suppressed because one or more lines are too long

873
.yarn/releases/yarn-3.5.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

9
.yarnrc.yml Normal file
View File

@ -0,0 +1,9 @@
nodeLinker: node-modules
npmRegistryServer: "https://npm.hibas123.de"
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.5.0.cjs

6
Backend/apidoc.json Normal file
View File

@ -0,0 +1,6 @@
{
"name": "openauth",
"description": "Open Auth REST API",
"title": "Open Auth REST",
"url": "/api"
}

View File

@ -38,5 +38,6 @@
"Login token invalid": "Login token invalid",
"No login token": "No login token",
"You are not logged in or your login is expired (Login token invalid)": "You are not logged in or your login is expired (Login token invalid)",
"You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)"
"You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)",
"Special token invalid": "Special token invalid"
}

17
Backend/locales/en.json Normal file
View File

@ -0,0 +1,17 @@
{
"Login": "Login",
"Username or Email": "Username or Email",
"Password": "Password",
"Next": "Next",
"Invalid code": "Invalid code",
"You are not logged in or your login is expired": "You are not logged in or your login is expired",
"User not found": "User not found",
"No special token": "No special token",
"You are not logged in or your login is expired(No special token)": "You are not logged in or your login is expired(No special token)",
"Special token invalid": "Special token invalid",
"You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)",
"No login token": "No login token",
"Login token invalid": "Login token invalid",
"Authorize %s": "Authorize %s",
"By clicking on ALLOW, you allow this app to access the requested recources.": "By clicking on ALLOW, you allow this app to access the requested recources."
}

71
Backend/package.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "@hibas123/openauth-backend",
"main": "lib/index.js",
"author": "Fabian Stamm <dev@fabianstamm.de>",
"license": "MIT",
"scripts": {
"build": "run-s build-ts build-doc",
"build-doc": "apidoc -i src/ -p apidoc/",
"build-ts": "tsc",
"start": "node lib/index.js",
"dev": "nodemon -e ts --exec ts-node src/index.ts",
"format": "prettier --write \"src/**\""
},
"pipelines": {
"install": [
"cd views && npm install",
"git submodule init",
"git submodule update",
"cd views_repo && npm install"
]
},
"devDependencies": {
"@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.3",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17",
"@types/i18n": "^0.13.6",
"@types/ini": "^1.3.31",
"@types/jsonwebtoken": "^9.0.1",
"@types/mongodb": "^3.6.20",
"@types/node": "^18.15.11",
"@types/node-rsa": "^1.1.1",
"@types/qrcode": "^1.5.0",
"@types/speakeasy": "^2.0.7",
"@types/uuid": "^9.0.1",
"apidoc": "^0.54.0",
"concurrently": "^8.0.1",
"nodemon": "^2.0.22",
"prettier": "^2.8.7",
"ts-node": "^10.9.1",
"typescript": "^5.0.3"
},
"dependencies": {
"@hibas123/config": "^1.1.2",
"@hibas123/nodelogging": "^3.1.3",
"@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/openauth-views-v1": "workspace:^",
"@hibas123/safe_mongo": "^1.7.1",
"body-parser": "^1.20.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"handlebars": "^4.7.7",
"i18n": "^0.15.1",
"ini": "^4.0.0",
"jsonwebtoken": "^9.0.0",
"moment": "^2.29.4",
"mongodb": "^3.7.3",
"node-rsa": "^1.1.1",
"npm-run-all": "^4.1.5",
"qrcode": "^1.5.1",
"reflect-metadata": "^0.1.13",
"speakeasy": "^2.0.0",
"u2f": "^0.1.3",
"uuid": "^9.0.0"
},
"packageManager": "yarn@3.5.0"
}

View File

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

View File

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

View File

@ -28,7 +28,7 @@ PermissionRoute.route("/")
promiseMiddleware(async (req, res) => {
let query = {};
if (req.query.client) {
query = { client: new ObjectID(req.query.client) };
query = { client: new ObjectID(req.query.client as string) };
}
let permissions = await Permission.find(query);
res.json(permissions);
@ -56,18 +56,18 @@ PermissionRoute.route("/")
verify(
{
client: {
type: Types.STRING
type: Types.STRING,
},
name: {
type: Types.STRING
type: Types.STRING,
},
description: {
type: Types.STRING
type: Types.STRING,
},
type: {
type: Types.ENUM,
values: ["user", "client"]
}
values: ["user", "client"],
},
},
true
),
@ -83,7 +83,7 @@ PermissionRoute.route("/")
description: req.body.description,
name: req.body.name,
client: client._id,
grant_type: req.body.type
grant_type: req.body.type,
});
await Permission.save(permission);
res.json(permission);
@ -102,7 +102,7 @@ PermissionRoute.route("/")
*/
.delete(
promiseMiddleware(async (req, res) => {
let { id } = req.query;
let { id } = req.query as { [key: string]: string };
await Permission.delete(id);
res.json({ success: true });
})

View File

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

View File

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

View File

@ -0,0 +1,110 @@
import { Request, Response, Router } from "express";
import Stacker from "../middlewares/stacker";
import {
GetClientAuthMiddleware,
GetClientApiAuthMiddleware,
} from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys";
import Client from "../../models/client";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import config from "../../config";
import Mail from "../../models/mail";
const ClientRouter = Router();
/**
* @api {get} /client/user
*
* @apiDescription Can be used for simple authentication of user. It will redirect the user to the redirect URI with a very short lived jwt.
*
* @apiParam {String} redirect_uri URL to redirect to on success
* @apiParam {String} state A optional state, that will be included in the JWT and redirect_uri as parameter
*
* @apiName ClientUser
* @apiGroup client
*
* @apiPermission user_client Requires ClientID and Authenticated User
*/
ClientRouter.get(
"/user",
Stacker(
GetClientAuthMiddleware(false),
GetUserMiddleware(false, false),
async (req: Request, res: Response) => {
let { redirect_uri, state } = req.query;
if (redirect_uri !== req.client.redirect_url)
throw new RequestError(
"Invalid redirect URI",
HttpStatusCode.BAD_REQUEST
);
let jwt = await createJWT(
{
client: req.client.client_id,
uid: req.user.uid,
username: req.user.username,
state: state,
},
{
expiresIn: 30,
issuer: config.core.url,
algorithm: "RS256",
subject: req.user.uid,
audience: req.client.client_id,
}
); //after 30 seconds this token is invalid
res.redirect(
redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : "")
);
}
)
);
ClientRouter.get(
"/account",
Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => {
let mails = await Promise.all(
req.user.mails.map((id) => Mail.findById(id))
);
let mail = mails.find((e) => e.primary) || mails[0];
res.json({
user: {
username: req.user.username,
name: req.user.name,
email: mail,
},
});
})
);
/**
* @api {get} /client/featured
*
* @apiDescription Get a list of clients, that want to be featured on the home page
*
* @apiName GetFeaturedClients
* @apiGroup client
*/
ClientRouter.get(
"/featured",
Stacker(async (req: Request, res) => {
let clients = await Client.find({
featured: true,
});
res.json({
clients: clients.map(({ name, logo, website, description }) => ({
name,
logo,
website,
description,
})),
});
})
);
export default ClientRouter;

View File

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

View File

@ -1,4 +1,4 @@
import * as express from "express"
import * as express from "express";
import AdminRoute from "./admin";
import UserRoute from "./user";
import InternalRoute from "./internal";
@ -6,10 +6,11 @@ import Login from "./user/login";
import ClientRouter from "./client";
import * as cors from "cors";
import OAuthRoute from "./oauth";
import config from "../config";
const ApiRouter: express.IRouter = express.Router();
ApiRouter.use("/admin", AdminRoute);
ApiRouter.use(cors())
ApiRouter.use(cors());
ApiRouter.use("/user", UserRoute);
ApiRouter.use("/internal", InternalRoute);
ApiRouter.use("/oauth", OAuthRoute);
@ -22,4 +23,11 @@ ApiRouter.use("/", ClientRouter);
// Legacy reasons (deprecated)
ApiRouter.post("/login", Login);
ApiRouter.get("/config.json", (req, res) => {
return res.json({
name: config.core.name,
url: config.core.url,
});
});
export default ApiRouter;

View File

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

View File

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

View File

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

View File

@ -5,8 +5,13 @@ import { validateJWT } from "../../keys";
import User from "../../models/user";
import Mail from "../../models/mail";
import { OAuthJWT } from "../../helper/jwt";
import Logging from "@hibas123/nodelogging";
export function GetClientAuthMiddleware(checksecret = true, internal = false, checksecret_if_available = false) {
export function GetClientAuthMiddleware(
checksecret = true,
internal = false,
checksecret_if_available = false
) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
let client_id = req.query.client_id || req.body.client_id;
@ -24,19 +29,29 @@ export function GetClientAuthMiddleware(checksecret = true, internal = false, ch
}
if (!client_id || (!client_secret && checksecret)) {
throw new RequestError("No client credentials", HttpStatusCode.BAD_REQUEST);
throw new RequestError(
"No client credentials",
HttpStatusCode.BAD_REQUEST
);
}
let w = { client_id: client_id, client_secret: client_secret };
if (!checksecret && !(checksecret_if_available && client_secret)) delete w.client_secret;
if (!checksecret && !(checksecret_if_available && client_secret))
delete w.client_secret;
let client = await Client.findOne(w)
let client = await Client.findOne(w);
if (!client) {
throw new RequestError("Invalid client_id" + (checksecret ? "or client_secret" : ""), HttpStatusCode.BAD_REQUEST);
throw new RequestError(
"Invalid client_id" + (checksecret ? "or client_secret" : ""),
HttpStatusCode.BAD_REQUEST
);
}
if (internal && !client.internal) {
throw new RequestError(req.__("Client has no permission for access"), HttpStatusCode.FORBIDDEN)
throw new RequestError(
req.__("Client has no permission for access"),
HttpStatusCode.FORBIDDEN
);
}
req.client = client;
next();
@ -44,7 +59,7 @@ export function GetClientAuthMiddleware(checksecret = true, internal = false, ch
if (next) next(e);
else throw e;
}
}
};
}
export const ClientAuthMiddleware = GetClientAuthMiddleware();
@ -52,10 +67,17 @@ export const ClientAuthMiddleware = GetClientAuthMiddleware();
export function GetClientApiAuthMiddleware(permissions?: string[]) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const invalid_err = new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED);
let token: string = req.query.access_token || req.headers.authorization;
if (!token)
const invalid_err = new RequestError(
req.__("Unauthorized"),
HttpStatusCode.UNAUTHORIZED
);
let token =
(req.query.access_token as string) ||
(req.headers.authorization as string);
if (!token) {
Logging.debug("No token found. Searched in query (access_token) and header (authorization)");
throw invalid_err;
}
if (token.toLowerCase().startsWith("bearer "))
token = token.substring(7);
@ -64,20 +86,31 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) {
try {
data = await validateJWT(token);
} catch (err) {
throw invalid_err
Logging.debug("Invalid JWT", err.message);
throw invalid_err;
}
let user = await User.findOne({ uid: data.user });
if (!user)
if (!user) {
Logging.debug("User not found");
throw invalid_err;
}
let client = await Client.findOne({ client_id: data.application })
if (!client)
let client = await Client.findOne({ client_id: data.application });
if (!client) {
Logging.debug("Client not found");
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))
) {
Logging.debug("Invalid permissions");
throw invalid_err;
}
req.user = user;
req.client = client;
@ -86,5 +119,5 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) {
if (next) next(e);
else throw e;
}
}
};
}

View File

@ -4,23 +4,25 @@ import promiseMiddleware from "../../helper/promiseMiddleware";
type RH = (req: Request, res: Response, next?: NextFunction) => any;
function call(handler: RH, req: Request, res: Response) {
return new Promise((yes, no) => {
return new Promise<void>((yes, no) => {
let p = handler(req, res, (err) => {
if (err) no(err);
else yes();
})
if (p && p.catch) p.catch(err => no(err));
})
});
if (p && p.catch) p.catch((err) => no(err));
});
}
const Stacker = (...handler: RH[]) => {
return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => {
return promiseMiddleware(
async (req: Request, res: Response, next: NextFunction) => {
let hc = handler.concat();
while (hc.length > 0) {
let h = hc.shift();
await call(h, req, res);
}
next();
});
}
}
);
};
export default Stacker;

View File

@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express";
import LoginToken, { CheckToken, ILoginToken } from "../../models/login_token";
import LoginToken, { CheckToken } from "../../models/login_token";
import Logging from "@hibas123/nodelogging";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user";
@ -31,7 +31,7 @@ export function GetUserMiddleware(
throw new Invalid(req.__(message));
};
try {
let { login, special } = req.query;
let { login, special } = req.query as { [key: string]: string };
if (!login) {
login = req.cookies.login;
special = req.cookies.special;
@ -50,8 +50,7 @@ export function GetUserMiddleware(
invalid("Login token invalid");
}
let special_token: ILoginToken;
let session: any;
let special_token;
if (special) {
Logging.debug("Special found");
special_token = await LoginToken.findOne({
@ -63,8 +62,6 @@ export function GetUserMiddleware(
if (!(await CheckToken(special_token, validated)))
invalid("Special token invalid");
req.special = true;
//
}
req.user = user;
@ -72,7 +69,6 @@ export function GetUserMiddleware(
req.token = {
login: token,
special: special_token,
session,
};
if (next) next();

View File

@ -1,6 +1,9 @@
import { Request, Response, NextFunction } from "express"
import { Logging } from "@hibas123/nodelogging";
import { isBoolean, isString, isNumber, isObject, isDate, isArray, isSymbol } from "util";
import { Request, Response, NextFunction } from "express";
import Logging from "@hibas123/nodelogging";
import {
isString,
isDate,
} from "util";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
export enum Types {
@ -11,39 +14,41 @@ export enum Types {
OBJECT,
DATE,
ARRAY,
ENUM
ENUM,
}
function isEmail(value: any): boolean {
return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value)
return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
value
);
}
export interface CheckObject {
type: Types
query?: boolean
optional?: boolean
type: Types;
query?: boolean;
optional?: boolean;
/**
* Only when Type.ENUM
*
* values to check before
*/
values?: string[]
values?: string[];
/**
* Only when Type.STRING
*/
notempty?: boolean // Only STRING
notempty?: boolean; // Only STRING
}
export interface Checks {
[index: string]: CheckObject// | Types
[index: string]: CheckObject; // | Types
}
// req: Request, res: Response, next: NextFunction
export default function (fields: Checks, noadditional = false) {
return (req: Request, res: Response, next: NextFunction) => {
let errors: { message: string, field: string }[] = []
let errors: { message: string; field: string }[] = [];
function check(data: any, field_name: string, field: CheckObject) {
if (data !== undefined && data !== null) {
@ -55,71 +60,83 @@ export default function (fields: Checks, noadditional = false) {
}
break;
case Types.NUMBER:
if (isNumber(data)) return;
if (typeof data == "number") return;
break;
case Types.EMAIL:
if (isEmail(data)) return;
break;
case Types.BOOLEAN:
if (isBoolean(data)) return;
if (typeof data == "boolean") return;
break;
case Types.OBJECT:
if (isObject(data)) return;
if (typeof data == "object") return;
break;
case Types.ARRAY:
if (isArray(data)) return;
if (Array.isArray(data)) return;
break;
case Types.DATE:
if (isDate(data)) return;
break;
case Types.ENUM:
if (isString(data)) {
if (typeof data == "string") {
if (field.values.indexOf(data) >= 0) return;
}
break;
default:
Logging.error(`Invalid type to check: ${field.type} ${Types[field.type]}`)
Logging.error(
`Invalid type to check: ${field.type} ${Types[field.type]}`
);
}
errors.push({
message: res.__("Field {{field}} has wrong type. It should be from type {{type}}", { field: field_name, type: Types[field.type].toLowerCase() }),
field: field_name
})
message: res.__(
"Field {{field}} has wrong type. It should be from type {{type}}",
{ field: field_name, type: Types[field.type].toLowerCase() }
),
field: field_name,
});
} else {
if (!field.optional) errors.push({
message: res.__("Field {{field}} is not defined", { field: field_name }),
field: field_name
})
if (!field.optional)
errors.push({
message: res.__("Field {{field}} is not defined", {
field: field_name,
}),
field: field_name,
});
}
}
for (let field_name in fields) {
let field = fields[field_name]
let data = fields[field_name].query ? req.query[field_name] : req.body[field_name]
check(data, field_name, field)
let field = fields[field_name];
let data = fields[field_name].query
? req.query[field_name]
: req.body[field_name];
check(data, field_name, field);
}
if (noadditional) { //Checks if the data given has additional parameters
if (noadditional) {
//Checks if the data given has additional parameters
let should = Object.keys(fields);
should = should.filter(e => !fields[e].query); //Query parameters should not exist on body
should = should.filter((e) => !fields[e].query); //Query parameters should not exist on body
let has = Object.keys(req.body);
has.every(e => {
has.every((e) => {
if (should.indexOf(e) >= 0) {
return true;
} else {
errors.push({
message: res.__("Field {{field}} should not be there", { field: e }),
field: e
})
message: res.__("Field {{field}} should not be there", {
field: e,
}),
field: e,
});
return false;
}
})
});
}
if (errors.length > 0) {
let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true);
next(err);
} else
next()
}
} else next();
};
}

View File

@ -91,16 +91,18 @@ const GetAuthRoute = (view = false) =>
response_type,
client_id,
redirect_uri,
scope,
scope = "",
state,
nored
} = req.query;
const sendError = type => {
nored,
} = req.query as { [key: string]: string };
const sendError = (type) => {
if (redirect_uri === "$local") redirect_uri = "/code";
res.redirect((redirect_uri += `?error=${type}&state=${state}`));
res.redirect(
(redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)
);
};
const scopes = scope.split(";");
const scopes = scope.split(";").filter((e: string) => e !== "");
Logging.debug("Scopes:", scope);
@ -123,7 +125,7 @@ const GetAuthRoute = (view = false) =>
let permissions: IPermission[] = [];
let proms: PromiseLike<void>[] = [];
if (scopes) {
for (let perm of scopes.filter(e => e !== "read_user")) {
for (let perm of scopes.filter((e) => e !== "read_user")) {
let oid = undefined;
try {
oid = new ObjectID(perm);
@ -132,7 +134,7 @@ const GetAuthRoute = (view = false) =>
continue;
}
proms.push(
Permission.findById(oid).then(p => {
Permission.findById(oid).then((p) => {
if (!p) return Promise.reject(new Error());
permissions.push(p);
})
@ -141,7 +143,7 @@ const GetAuthRoute = (view = false) =>
}
let err = undefined;
await Promise.all(proms).catch(e => {
await Promise.all(proms).catch((e) => {
err = e;
});
@ -152,7 +154,7 @@ const GetAuthRoute = (view = false) =>
let grant: IGrant | undefined = await Grant.findOne({
client: client._id,
user: req.user._id
user: req.user._id,
});
Logging.debug("Grant", grant, permissions);
@ -161,14 +163,14 @@ const GetAuthRoute = (view = false) =>
if (grant) {
missing_permissions = grant.permissions
.map(perm => permissions.find(p => p._id.equals(perm)))
.filter(e => !!e);
.map((perm) => permissions.find((p) => p._id.equals(perm)))
.filter((e) => !!e);
} else {
missing_permissions = permissions;
}
let client_granted_perm = missing_permissions.filter(
e => e.grant_type == "client"
(e) => e.grant_type == "client"
);
if (client_granted_perm.length > 0) {
return sendError("no_permission");
@ -176,8 +178,10 @@ const GetAuthRoute = (view = false) =>
if (!grant && missing_permissions.length > 0) {
await new Promise<void>((yes, no) =>
GetUserMiddleware(false, true)(req, res, (err?: Error) =>
err ? no(err) : yes()
GetUserMiddleware(false, true)(
req,
res,
(err?: Error | string) => (err ? no(err) : yes())
)
); // Maybe unresolved when redirect is happening
@ -186,11 +190,11 @@ const GetAuthRoute = (view = false) =>
GetAuthPage(
req.__,
client.name,
permissions.map(perm => {
permissions.map((perm) => {
return {
name: perm.name,
description: perm.description,
logo: client.logo
logo: client.logo,
};
})
)
@ -202,11 +206,11 @@ const GetAuthRoute = (view = false) =>
grant = Grant.new({
client: client._id,
user: req.user._id,
permissions: []
permissions: [],
});
grant.permissions.push(
...missing_permissions.map(e => e._id)
...missing_permissions.map((e) => e._id)
);
await Grant.save(grant);
} else {
@ -218,20 +222,19 @@ const GetAuthRoute = (view = false) =>
let code = ClientCode.new({
user: req.user._id,
client: client._id,
permissions: permissions.map(p => p._id),
validTill: moment()
.add(30, "minutes")
.toDate(),
code: randomBytes(16).toString("hex")
permissions: permissions.map((p) => p._id),
validTill: moment().add(30, "minutes").toDate(),
code: randomBytes(16).toString("hex"),
});
await ClientCode.save(code);
let redir =
client.redirect_url === "$local" ? "/code" : client.redirect_url;
let ruri = redir + `?code=${code.code}&state=${state}`;
let ruri =
redir + `?code=${code.code}${state ? "&state=" + state : ""}`;
if (nored === "true") {
res.json({
redirect_uri: ruri
redirect_uri: ruri,
});
} else {
res.redirect(ruri);

View File

@ -3,8 +3,9 @@ import GetAuthRoute from "./auth";
import JWTRoute from "./jwt";
import Public from "./public";
import RefreshTokenRoute from "./refresh";
import ProfileRoute from "./profile";
const OAuthRoue: Router = Router();
const OAuthRoute: Router = Router();
/**
* @api {post} /oauth/auth
* @apiName OAuthAuth
@ -19,7 +20,7 @@ const OAuthRoue: Router = Router();
* @apiParam {String} state State, that will be passed to redirect_uri for client
* @apiParam {String} nored Deactivates the Redirect response from server and instead returns the redirect URI in JSON response
*/
OAuthRoue.post("/auth", GetAuthRoute(false));
OAuthRoute.post("/auth", GetAuthRoute(false));
/**
* @api {get} /oauth/jwt
@ -32,7 +33,7 @@ OAuthRoue.post("/auth", GetAuthRoute(false));
*
* @apiSuccess {String} token The JWT that allowes the application to access the recources granted for refresh token
*/
OAuthRoue.get("/jwt", JWTRoute)
OAuthRoute.get("/jwt", JWTRoute);
/**
* @api {get} /oauth/public
@ -43,7 +44,7 @@ OAuthRoue.get("/jwt", JWTRoute)
*
* @apiSuccess {String} public_key The applications public_key. Used to verify JWT.
*/
OAuthRoue.get("/public", Public)
OAuthRoute.get("/public", Public);
/**
* @api {get} /oauth/refresh
@ -51,7 +52,7 @@ OAuthRoue.get("/public", Public)
*
* @apiGroup oauth
*/
OAuthRoue.get("/refresh", RefreshTokenRoute);
OAuthRoute.get("/refresh", RefreshTokenRoute);
/**
* @api {post} /oauth/refresh
@ -59,5 +60,14 @@ OAuthRoue.get("/refresh", RefreshTokenRoute);
*
* @apiGroup oauth
*/
OAuthRoue.post("/refresh", RefreshTokenRoute);
export default OAuthRoue;
OAuthRoute.post("/refresh", RefreshTokenRoute);
/**
* @api {get} /oauth/profile
* @apiName OAuthProfile
*
* @apiGroup oauth
*/
OAuthRoute.get("/profile", ProfileRoute);
export default OAuthRoute;

View File

@ -7,22 +7,37 @@ import Client from "../../models/client";
import { getAccessTokenJWT } from "../../helper/jwt";
const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
let { refreshtoken } = req.query;
if (!refreshtoken) throw new RequestError(req.__("Refresh token not set"), HttpStatusCode.BAD_REQUEST);
let { refreshtoken } = req.query as { [key: string]: string };
if (!refreshtoken)
throw new RequestError(
req.__("Refresh token not set"),
HttpStatusCode.BAD_REQUEST
);
let token = await RefreshToken.findOne({ token: refreshtoken });
if (!token) throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
if (!token)
throw new RequestError(
req.__("Invalid token"),
HttpStatusCode.BAD_REQUEST
);
let user = await User.findById(token.user);
if (!user) {
token.valid = false;
await RefreshToken.save(token);
throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
throw new RequestError(
req.__("Invalid token"),
HttpStatusCode.BAD_REQUEST
);
}
let client = await Client.findById(token.client);
let jwt = await getAccessTokenJWT({ user, permissions: token.permissions, client });
let jwt = await getAccessTokenJWT({
user,
permissions: token.permissions,
client,
});
res.json({ token: jwt });
})
});
export default JWTRoute;

View File

@ -0,0 +1,23 @@
import Mail from "../../models/mail";
import { GetClientApiAuthMiddleware } from "../middlewares/client";
import Stacker from "../middlewares/stacker";
import { Request, Response } from "express";
export default Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => {
let mails = await Promise.all(
req.user.mails.map((id) => Mail.findById(id))
);
let mail = mails.find((e) => e.primary) || mails[0];
res.json({
user_id: req.user.uid,
id: req.user.uid,
ID: req.user.uid,
sub: req.user.uid,
email: mail.mail,
username: req.user.username,
displayName: req.user.name,
displayNameClaim: req.user.name,
});
})

View File

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

View File

@ -0,0 +1,122 @@
import { Request, Response } from "express";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user";
import Client from "../../models/client";
import {
getAccessTokenJWT,
getIDToken,
AccessTokenJWTExp,
} from "../../helper/jwt";
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client";
import ClientCode from "../../models/client_code";
import Mail from "../../models/mail";
import { randomBytes } from "crypto";
import moment = require("moment");
// import { JWTExpDur } from "../../keys";
import RefreshToken from "../../models/refresh_token";
import { getEncryptionKey } from "../../helper/user_key";
import { refreshTokenValidTime } from "../../config";
// TODO:
/*
For example, the authorization server could employ refresh token
rotation in which a new refresh token is issued with every access
token refresh response. The previous refresh token is invalidated but retained by the authorization server. If a refresh token is
compromised and subsequently used by both the attacker and the
legitimate client, one of them will present an invalidated refresh
token, which will inform the authorization server of the breach.
*/
const RefreshTokenRoute = Stacker(
GetClientAuthMiddleware(false, false, true),
async (req: Request, res: Response) => {
let grant_type = req.query.grant_type || req.body.grant_type;
if (!grant_type || grant_type === "authorization_code") {
let code = req.query.code || req.body.code;
let nonce = req.query.nonce || req.body.nonce;
let c = await ClientCode.findOne({ code: code });
if (!c || moment(c.validTill).isBefore()) {
throw new RequestError(
req.__("Invalid code"),
HttpStatusCode.BAD_REQUEST
);
}
let client = await Client.findById(c.client);
let user = await User.findById(c.user);
let mails = await Promise.all(user.mails.map((m) => Mail.findOne(m)));
let token = RefreshToken.new({
user: c.user,
client: c.client,
permissions: c.permissions,
token: randomBytes(16).toString("hex"),
valid: true,
validTill: moment().add(refreshTokenValidTime).toDate(),
});
await RefreshToken.save(token);
await ClientCode.delete(c);
let mail = mails.find((e) => e.primary);
if (!mail) mail = mails[0];
res.json({
refresh_token: token.token,
token: token.token,
access_token: await getAccessTokenJWT({
client: client,
user: user,
permissions: c.permissions,
}),
token_type: "bearer",
expires_in: AccessTokenJWTExp.asSeconds(),
profile: {
uid: user.uid,
email: mail ? mail.mail : "",
name: user.name,
enc_key: getEncryptionKey(user, client),
},
id_token: getIDToken(user, client.client_id, nonce),
});
} else if (grant_type === "refresh_token") {
let refresh_token = req.query.refresh_token || req.body.refresh_token;
if (!refresh_token)
throw new RequestError(
req.__("refresh_token not set"),
HttpStatusCode.BAD_REQUEST
);
let token = await RefreshToken.findOne({ token: refresh_token });
if (!token || !token.valid || moment(token.validTill).isBefore())
throw new RequestError(
req.__("Invalid token"),
HttpStatusCode.BAD_REQUEST
);
token.validTill = moment().add(refreshTokenValidTime).toDate();
await RefreshToken.save(token);
let user = await User.findById(token.user);
let client = await Client.findById(token.client);
let jwt = await getAccessTokenJWT({
user,
client,
permissions: token.permissions,
});
res.json({
access_token: jwt,
expires_in: AccessTokenJWTExp.asSeconds(),
});
} else {
throw new RequestError(
"invalid grant_type",
HttpStatusCode.BAD_REQUEST
);
}
}
);
export default RefreshTokenRoute;

View File

@ -0,0 +1,19 @@
import { Request, Response } from "express";
import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user";
import LoginToken, { CheckToken } from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
export const GetAccount = Stacker(
GetUserMiddleware(true, true),
async (req: Request, res: Response) => {
let user = {
id: req.user.uid,
name: req.user.name,
username: req.user.username,
birthday: req.user.birthday,
gender: req.user.gender,
};
res.json({ user });
}
);

View File

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

View File

@ -1,10 +1,11 @@
import { Router } from "express";
import Register from "./register";
import Login from "./login";
import TwoFactorRoute from "./twofactor";
import { GetToken, DeleteToken } from "./token";
import { GetAccount } from "./account";
import { GetContactInfos } from "./contact";
import Login from "./login";
import Register from "./register";
import { DeleteToken, GetToken } from "./token";
import TwoFactorRoute from "./twofactor";
import OAuthRoute from "./oauth";
const UserRoute: Router = Router();
@ -62,7 +63,7 @@ UserRoute.post("/register", Register);
* @apiSuccess {String} tfa.name The name of the TFA Method
* @apiSuccess {String} tfa.type The type of the TFA Method
*/
UserRoute.post("/login", Login)
UserRoute.post("/login", Login);
UserRoute.use("/twofactor", TwoFactorRoute);
/**
@ -95,7 +96,6 @@ UserRoute.get("/token", GetToken);
*/
UserRoute.delete("/token/:id", DeleteToken);
/**
* @api {delete} /user/account
* @apiName UserGetAccount
@ -126,4 +126,7 @@ UserRoute.get("/account", GetAccount);
* @apiSuccess {Object[]} user.phone Phone numbers
*/
UserRoute.get("/contact", GetContactInfos);
UserRoute.use("/oauth", OAuthRoute);
export default UserRoute;

View File

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

View File

@ -0,0 +1,21 @@
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import Client, { IClient } from "../../../models/client";
export async function getClientWithOrigin(client_id: string, origin: string) {
const client = await Client.findOne({
client_id,
});
const clientNotFoundError = new RequestError(
"Client not found!",
HttpStatusCode.BAD_REQUEST
);
if (!client) throw clientNotFoundError;
const clientUrl = new URL(client.redirect_url);
if (clientUrl.hostname !== origin) throw clientNotFoundError;
return client;
}

View File

@ -0,0 +1,12 @@
import { Router } from "express";
import { GetJWTByUser } from "./jwt";
import { GetPermissionsForAuthRequest } from "./permissions";
import { GetTokenByUser } from "./refresh_token";
const router = Router();
router.get("/jwt", GetJWTByUser);
router.get("/permissions", GetPermissionsForAuthRequest);
router.get("/refresh_token", GetTokenByUser);
export default router;

View File

@ -0,0 +1,25 @@
import { Request, Response } from "express";
import Stacker from "../../middlewares/stacker";
import { GetUserMiddleware } from "../../middlewares/user";
import { URL } from "url";
import Client from "../../../models/client";
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import { getAccessTokenJWT } from "../../../helper/jwt";
import { getClientWithOrigin } from "./_helper";
export const GetJWTByUser = Stacker(
GetUserMiddleware(true, false),
async (req: Request, res: Response) => {
const { client_id, origin } = req.query as { [key: string]: string };
const client = await getClientWithOrigin(client_id, origin);
const jwt = await getAccessTokenJWT({
user: req.user,
client: client,
permissions: [],
});
res.json({ jwt });
}
);

View File

@ -0,0 +1,38 @@
import { Request, Response } from "express";
import Stacker from "../../middlewares/stacker";
import { GetUserMiddleware } from "../../middlewares/user";
import { URL } from "url";
import Client from "../../../models/client";
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import { randomBytes } from "crypto";
import moment = require("moment");
import RefreshToken from "../../../models/refresh_token";
import { refreshTokenValidTime } from "../../../config";
import { getClientWithOrigin } from "./_helper";
import Permission from "../../../models/permissions";
export const GetPermissionsForAuthRequest = Stacker(
GetUserMiddleware(true, false),
async (req: Request, res: Response) => {
const { client_id, origin, permissions } = req.query as {
[key: string]: string;
};
const client = await getClientWithOrigin(client_id, origin);
const perm = permissions.split(",").filter((e) => !!e);
const resolved = await Promise.all(
perm.map((p) => Permission.findById(p))
);
if (resolved.some((e) => e.grant_type !== "user")) {
throw new RequestError(
"Invalid Permission requested",
HttpStatusCode.BAD_REQUEST
);
}
res.json({ permissions: resolved });
}
);

View File

@ -0,0 +1,49 @@
import { Request, Response } from "express";
import Stacker from "../../middlewares/stacker";
import { GetUserMiddleware } from "../../middlewares/user";
import { URL } from "url";
import Client from "../../../models/client";
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import { randomBytes } from "crypto";
import moment = require("moment");
import RefreshToken from "../../../models/refresh_token";
import { refreshTokenValidTime } from "../../../config";
import { getClientWithOrigin } from "./_helper";
import Permission from "../../../models/permissions";
export const GetTokenByUser = Stacker(
GetUserMiddleware(true, false),
async (req: Request, res: Response) => {
const { client_id, origin, permissions } = req.query as {
[key: string]: string;
};
const client = await getClientWithOrigin(client_id, origin);
const perm = permissions.split(",").filter((e) => !!e);
const resolved = await Promise.all(
perm.map((p) => Permission.findById(p))
);
if (resolved.some((e) => e.grant_type !== "user")) {
throw new RequestError(
"Invalid Permission requested",
HttpStatusCode.BAD_REQUEST
);
}
let token = RefreshToken.new({
user: req.user._id,
client: client._id,
permissions: resolved.map((e) => e._id),
token: randomBytes(16).toString("hex"),
valid: true,
validTill: moment().add(refreshTokenValidTime).toDate(),
});
await RefreshToken.save(token);
res.json({ token });
}
);

View File

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

View File

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

View File

@ -0,0 +1,100 @@
import { Router } from "express";
import Stacker from "../../../middlewares/stacker";
import { GetUserMiddleware } from "../../../middlewares/user";
import TwoFactor, {
TFATypes as TwoFATypes,
IBackupCode,
} from "../../../../models/twofactor";
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment");
import { upgradeToken } from "../helper";
import * as crypto from "crypto";
import Logging from "@hibas123/nodelogging";
const BackupCodeRoute = Router();
// TODO: Further checks if this is good enough randomness
function generateCode(length: number) {
let bytes = crypto.randomBytes(length);
let nrs = "";
bytes.forEach((b, idx) => {
let nr = Math.floor((b / 255) * 9.9999);
if (nr > 9) nr = 9;
nrs += String(nr);
});
return nrs;
}
BackupCodeRoute.post(
"/",
Stacker(GetUserMiddleware(true, true), async (req, res) => {
//Generating new
let codes = Array(10).map(() => generateCode(8));
console.log(codes);
let twofactor = TwoFactor.new(<IBackupCode>{
user: req.user._id,
type: TwoFATypes.OTC,
valid: true,
data: codes,
name: "",
});
await TwoFactor.save(twofactor);
res.json({
codes,
id: twofactor._id,
});
})
);
BackupCodeRoute.put(
"/",
Stacker(
GetUserMiddleware(true, false, undefined, false),
async (req, res) => {
let { login, special } = req.token;
let { id, code }: { id: string; code: string } = req.body;
let twofactor: IBackupCode = await TwoFactor.findById(id);
if (
!twofactor ||
!twofactor.valid ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.OTC
) {
throw new RequestError(
"Invalid Method!",
HttpStatusCode.BAD_REQUEST
);
}
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
twofactor.valid = false;
await TwoFactor.save(twofactor);
throw new RequestError(
"Invalid Method!",
HttpStatusCode.BAD_REQUEST
);
}
code = code.replace(/\s/g, "");
let valid = twofactor.data.find((c) => c === code);
if (valid) {
twofactor.data = twofactor.data.filter((c) => c !== code);
await TwoFactor.save(twofactor);
let [login_exp, special_exp] = await Promise.all([
upgradeToken(login),
upgradeToken(special),
]);
res.json({ success: true, login_exp, special_exp });
} else {
throw new RequestError(
"Invalid or already used code!",
HttpStatusCode.BAD_REQUEST
);
}
}
)
);
export default BackupCodeRoute;

View File

@ -6,7 +6,10 @@ export async function upgradeToken(token: ILoginToken) {
token.valid = true;
token.validated = true;
//TODO durations from config
let expires = (token.special ? moment().add(30, "minute") : moment().add(6, "months")).toDate();
let expires = (token.special
? moment().add(30, "minute")
: moment().add(6, "months")
).toDate();
token.validTill = expires;
await LoginToken.save(token);
return expires;

View File

@ -0,0 +1,56 @@
import { Router } from "express";
import YubiKeyRoute from "./yubikey";
import { GetUserMiddleware } from "../../middlewares/user";
import Stacker from "../../middlewares/stacker";
import TwoFactor from "../../../models/twofactor";
import * as moment from "moment";
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import OTCRoute from "./otc";
import BackupCodeRoute from "./backup";
const TwoFactorRouter = Router();
TwoFactorRouter.get(
"/",
Stacker(GetUserMiddleware(true, true), async (req, res) => {
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true });
let expired = twofactor.filter((e) =>
e.expires ? moment().isAfter(moment(e.expires)) : false
);
await Promise.all(
expired.map((e) => {
e.valid = false;
return TwoFactor.save(e);
})
);
twofactor = twofactor.filter((e) => e.valid);
let tfa = twofactor.map((e) => {
return {
id: e._id,
name: e.name,
type: e.type,
};
});
res.json({ methods: tfa });
})
);
TwoFactorRouter.delete(
"/:id",
Stacker(GetUserMiddleware(true, true), async (req, res) => {
let { id } = req.params;
let tfa = await TwoFactor.findById(id);
if (!tfa || !tfa.user.equals(req.user._id)) {
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
}
tfa.valid = false;
await TwoFactor.save(tfa);
res.json({ success: true });
})
);
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
TwoFactorRouter.use("/otc", OTCRoute);
TwoFactorRouter.use("/backup", BackupCodeRoute);
export default TwoFactorRouter;

View File

@ -119,11 +119,6 @@ OTCRoute.put(
token: code,
});
if (!valid && config.core.dev === true && code === "000000") {
Logging.warning("Dev mode enabled and dev OTC used.");
valid = true;
}
if (valid) {
let [login_exp, special_exp] = await Promise.all([
upgradeToken(login),

View File

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

75
Backend/src/config.ts Normal file
View File

@ -0,0 +1,75 @@
import { parse } from "@hibas123/config";
import Logging from "@hibas123/nodelogging";
import * as dotenv from "dotenv";
import moment = require("moment");
export const refreshTokenValidTime = moment.duration(6, "month");
dotenv.config();
export interface DatabaseConfig {
host: string;
database: string;
}
export interface WebConfig {
port: string;
secure: "true" | "false" | undefined;
}
export interface CoreConfig {
name: string;
url: string;
dev: boolean;
}
export interface Config {
core: CoreConfig;
database: DatabaseConfig;
web: WebConfig;
}
const config = (parse(
{
core: {
dev: {
default: false,
type: Boolean,
},
name: {
type: String,
default: "Open Auth",
},
url: String,
},
database: {
database: {
type: String,
default: "openauth",
},
host: {
type: String,
default: "localhost",
},
},
web: {
port: {
type: Number,
default: 3004,
},
secure: {
type: Boolean,
default: false,
},
},
},
"config.ini"
) as any) as Config;
if (process.env.DEV === "true") config.core.dev = true;
if (config.core.dev)
Logging.warning(
"DEV mode active. This can cause major performance issues, data loss and vulnerabilities! "
);
export default config;

View File

@ -1,19 +1,13 @@
import SafeMongo from "@hibas123/safe_mongo";
import Config from "./config";
let dbname = "openauth";
let host = "localhost";
if (Config.database) {
if (Config.database.database) dbname = Config.database.database;
if (Config.database.host) host = Config.database.host;
}
if (Config.core.dev) dbname += "_dev";
const DB = new SafeMongo("mongodb://" + host, dbname, {
useUnifiedTopology: true,
useNewUrlParser: true,
} as any);
});
export default DB;

View File

@ -11,7 +11,6 @@ declare module "express" {
token: {
login: ILoginToken;
special?: ILoginToken;
session?: { [key: string]: any };
};
}
}

60
Backend/src/helper/jwt.ts Normal file
View File

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

View File

@ -0,0 +1,7 @@
import { Request, Response, NextFunction } from "express";
export default (
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.
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
*/
export enum HttpStatusCode {
/**
* The server has received the request headers and the client should proceed to send the request body
* (in the case of a request for which a body needs to be sent; for example, a POST request).
@ -376,13 +374,17 @@ export enum HttpStatusCode {
* Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used
* to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot).
*/
NETWORK_AUTHENTICATION_REQUIRED = 511
NETWORK_AUTHENTICATION_REQUIRED = 511,
}
export default class RequestError extends Error {
constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false, public additional: any = undefined) {
super("")
constructor(
message: any,
public status: HttpStatusCode,
public nolog: boolean = false,
public additional: any = undefined
) {
super("");
this.message = message;
}
}

View File

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

90
Backend/src/index.ts Normal file
View File

@ -0,0 +1,90 @@
import Logging from "@hibas123/nodelogging";
import config from "./config";
// import NLS from "@hibas123/nodeloggingserver_client";
// if (config.logging) {
// let s = NLS(Logging, config.logging.server, config.logging.appid, config.logging.token);
// s.send(`[${new Date().toLocaleTimeString()}] Starting application`);
// }
// if (!config.database) {
// Logging.error("No database config set. Terminating.")
// process.exit();
// }
if (!config.web) {
Logging.error("No web config set. Terminating.");
process.exit();
}
import * as i18n from "i18n";
i18n.configure({
locales: ["en", "de"],
directory: "./locales",
});
import Web from "./web";
import TestData from "./testdata";
import DB from "./database";
Logging.log("Connecting to Database");
if (config.core.dev) {
Logging.warning("Running in dev mode! Database will be cleared!");
}
DB.connect()
.then(async () => {
Logging.log("Database connected", config);
if (config.core.dev) await TestData();
let web = new Web(config.web);
web.listen();
let already = new Set();
function print(path, layer) {
if (layer.route) {
layer.route.stack.forEach(
print.bind(null, path.concat(split(layer.route.path)))
);
} else if (layer.name === "router" && layer.handle.stack) {
layer.handle.stack.forEach(
print.bind(null, path.concat(split(layer.regexp)))
);
} else if (layer.method) {
let me: string = layer.method.toUpperCase();
me += " ".repeat(6 - me.length);
let msg = `${me} /${path
.concat(split(layer.regexp))
.filter(Boolean)
.join("/")}`;
if (!already.has(msg)) {
already.add(msg);
Logging.log(msg);
}
}
}
function split(thing) {
if (typeof thing === "string") {
return thing.split("/");
} else if (thing.fast_slash) {
return "";
} else {
var match = thing
.toString()
.replace("\\/?", "")
.replace("(?=\\/|$)", "$")
.match(
/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//
);
return match
? match[1].replace(/\\(.)/g, "$1").split("/")
: "<complex:" + thing.toString() + ">";
}
}
// Logging.log("--- Endpoints: ---");
// web.server._router.stack.forEach(print.bind(null, []))
// Logging.log("--- Endpoints end ---")
})
.catch((e) => {
Logging.error(e);
process.exit();
});

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,76 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import moment = require("moment");
export interface ILoginToken extends ModelDataBase {
token: string;
special: boolean;
user: ObjectID;
validTill: Date;
valid: boolean;
validated: boolean;
data: any;
ip: string;
browser: string;
}
const LoginToken = DB.addModel<ILoginToken>({
name: "login_token",
versions: [
{
migration: () => {},
schema: {
token: { type: String },
special: { type: Boolean, default: () => false },
user: { type: ObjectID },
validTill: { type: Date },
valid: { type: Boolean },
},
},
{
migration: (doc: ILoginToken) => {
doc.validated = true;
},
schema: {
token: { type: String },
special: { type: Boolean, default: () => false },
user: { type: ObjectID },
validTill: { type: Date },
valid: { type: Boolean },
validated: { type: Boolean, default: false },
},
},
{
migration: (doc: ILoginToken) => {
doc.validated = true;
},
schema: {
token: { type: String },
special: { type: Boolean, default: () => false },
user: { type: ObjectID },
validTill: { type: Date },
valid: { type: Boolean },
validated: { type: Boolean, default: false },
data: { type: "any", optional: true },
ip: { type: String, optional: true },
browser: { type: String, optional: true },
},
},
],
});
export async function CheckToken(
token: ILoginToken,
validated: boolean = true
): Promise<boolean> {
if (!token || !token.valid) return false;
if (validated && !token.validated) return false;
if (moment().isAfter(token.validTill)) {
token.valid = false;
await LoginToken.save(token);
return false;
}
return true;
}
export default LoginToken;

View File

@ -0,0 +1,24 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo";
export interface IMail extends ModelDataBase {
mail: string;
verified: boolean;
primary: boolean;
}
const Mail = DB.addModel<IMail>({
name: "mail",
versions: [
{
migration: () => {},
schema: {
mail: { type: String },
verified: { type: Boolean, default: false },
primary: { type: Boolean },
},
},
],
});
export default Mail;

View File

@ -0,0 +1,37 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
export interface IPermission extends ModelDataBase {
name: string;
description: string;
client: ObjectID;
grant_type: "user" | "client";
}
const Permission = DB.addModel<IPermission>({
name: "permission",
versions: [
{
migration: () => {},
schema: {
name: { type: String },
description: { type: String },
client: { type: ObjectID },
},
},
{
migration: (old) => {
old.grant_type = "user";
},
schema: {
name: { type: String },
description: { type: String },
client: { type: ObjectID },
grant_type: { type: String, default: "user" },
},
},
],
});
export default Permission;

View File

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

View File

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

View File

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

134
Backend/src/models/user.ts Normal file
View File

@ -0,0 +1,134 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
import { randomString } from "../helper/random";
export enum Gender {
none,
male,
female,
other,
}
export interface IUser extends ModelDataBase {
uid: string;
username: string;
name: string;
birthday?: Date;
gender: Gender;
admin: boolean;
password: string;
salt: string;
mails: ObjectID[];
phones: { phone: string; verified: boolean; primary: boolean }[];
encryption_key: string;
}
const User = DB.addModel<IUser>({
name: "user",
versions: [
{
migration: () => {},
schema: {
uid: { type: String, default: () => v4() },
username: { type: String },
name: { type: String },
birthday: { type: Date, optional: true },
gender: { type: Number },
admin: { type: Boolean },
password: { type: String },
salt: { type: String },
mails: { type: Array, default: () => [] },
phones: {
array: true,
model: true,
type: {
phone: { type: String },
verified: { type: Boolean },
primary: { type: Boolean },
},
},
twofactor: {
array: true,
model: true,
type: {
token: { type: String },
valid: { type: Boolean },
type: { type: Number },
},
},
},
},
{
migration: (e: IUser) => {
e.encryption_key = randomString(64);
},
schema: {
uid: { type: String, default: () => v4() },
username: { type: String },
name: { type: String },
birthday: { type: Date, optional: true },
gender: { type: Number },
admin: { type: Boolean },
password: { type: String },
salt: { type: String },
mails: { type: Array, default: () => [] },
phones: {
array: true,
model: true,
type: {
phone: { type: String },
verified: { type: Boolean },
primary: { type: Boolean },
},
},
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;
},
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;

View File

@ -1,6 +1,6 @@
import User, { Gender } from "./models/user";
import Client from "./models/client";
import { Logging } from "@hibas123/nodelogging";
import Logging from "@hibas123/nodelogging";
import RegCode from "./models/regcodes";
import * as moment from "moment";
import Permission from "./models/permissions";
@ -8,12 +8,12 @@ import { ObjectID } from "bson";
import DB from "./database";
import TwoFactor from "./models/twofactor";
import * as speakeasy from "speakeasy";
import LoginToken from "./models/login_token";
import Mail from "./models/mail";
export default async function TestData() {
Logging.warn("Running in dev mode! Database will be cleared!");
await DB.db.dropDatabase();
let mail = await Mail.findOne({ mail: "test@test.de" });
@ -21,13 +21,12 @@ export default async function TestData() {
mail = Mail.new({
mail: "test@test.de",
primary: true,
verified: true
})
verified: true,
});
await Mail.save(mail);
}
let u = await User.findOne({ username: "test" });
if (!u) {
Logging.log("Adding test user");
@ -36,21 +35,22 @@ export default async function TestData() {
birthday: new Date(),
gender: Gender.male,
name: "Test Test",
password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
password:
"125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
salt: "test",
admin: true,
phones: [
{ phone: "+4915962855955", primary: true, verified: true },
{ phone: "+4915962855932", primary: false, verified: false }
{ phone: "+4915962855932", primary: false, verified: false },
],
mails: [mail._id]
})
mails: [mail._id],
});
await User.save(u);
}
let c = await Client.findOne({ client_id: "test001" });
if (!c) {
Logging.log("Adding test client")
Logging.log("Adding test client");
c = Client.new({
client_id: "test001",
client_secret: "test001",
@ -58,19 +58,22 @@ export default async function TestData() {
maintainer: u._id,
name: "Test Client",
website: "http://example.com",
redirect_url: "http://example.com"
})
redirect_url: "http://example.com",
featured: true,
description:
"This client is just for testing purposes. It does not have any functionality.",
});
await Client.save(c);
}
let perm = await Permission.findOne({ id: 0 });
let perm = await Permission.findById("507f1f77bcf86cd799439011");
if (!perm) {
Logging.log("Adding test permission")
Logging.log("Adding test permission");
perm = Permission.new({
_id: new ObjectID("507f1f77bcf86cd799439011"),
name: "TestPerm",
description: "Permission just for testing purposes",
client: c._id
client: c._id,
});
await (await (Permission as any)._collection).insertOne(perm);
@ -80,30 +83,29 @@ export default async function TestData() {
let r = await RegCode.findOne({ token: "test" });
if (!r) {
Logging.log("Adding test reg_code")
Logging.log("Adding test reg_code");
r = RegCode.new({
token: "test",
valid: true,
validTill: moment().add("1", "year").toDate()
})
validTill: moment().add("1", "year").toDate(),
});
await RegCode.save(r);
}
let t = await TwoFactor.findOne({ user: u._id, type: 0 })
let t = await TwoFactor.findOne({ user: u._id, type: 0 });
if (!t) {
t = TwoFactor.new({
user: u._id,
type: 0,
valid: true,
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
expires: null
})
TwoFactor.save(t);
expires: null,
});
await TwoFactor.save(t);
}
let login_token = await LoginToken.findOne({ token: "test01" });
if (login_token)
await LoginToken.delete(login_token);
if (login_token) await LoginToken.delete(login_token);
login_token = LoginToken.new({
browser: "DEMO",
@ -113,13 +115,12 @@ export default async function TestData() {
valid: true,
validTill: moment().add("10", "years").toDate(),
user: u._id,
validated: true
validated: true,
});
await LoginToken.save(login_token);
let special_token = await LoginToken.findOne({ token: "test02" });
if (special_token)
await LoginToken.delete(special_token);
if (special_token) await LoginToken.delete(special_token);
special_token = LoginToken.new({
browser: "DEMO",
@ -129,11 +130,10 @@ export default async function TestData() {
valid: true,
validTill: moment().add("10", "years").toDate(),
user: u._id,
validated: true
validated: true,
});
await LoginToken.save(special_token);
// setInterval(() => {
// let code = speakeasy.totp({
// secret: t.data,
@ -141,4 +141,6 @@ export default async function TestData() {
// })
// Logging.debug("OTC Code is:", code);
// }, 1000)
console.log("Finished adding test data")
}

View File

@ -0,0 +1,8 @@
import { __ as i__ } from "i18n";
import config from "../config";
import * as viewsv1 from "@hibas123/openauth-views-v1";
export default function GetAdminPage(__: typeof i__): string {
let data = {};
return viewsv1.admin(config.core.dev)(data, { helpers: { i18n: __ } });
}

View File

@ -0,0 +1,22 @@
import { __ as i__ } from "i18n";
import config from "../config";
import * as viewsv1 from "@hibas123/openauth-views-v1";
export default function GetAuthPage(
__: typeof i__,
appname: string,
scopes: { name: string; description: string; logo: string }[]
): string {
return viewsv1.authorize(config.core.dev)(
{
title: __("Authorize %s", appname),
information: __(
"By clicking on ALLOW, you allow this app to access the requested recources."
),
scopes: scopes,
// request: request
},
{ helpers: { i18n: __ } }
);
}

View File

@ -0,0 +1,9 @@
import { __ as i__ } from "i18n";
import config from "../config";
import * as viewsv1 from "@hibas123/openauth-views-v1";
export default function GetRegistrationPage(__: typeof i__): string {
let data = {};
return viewsv1.register(config.core.dev)(data, { helpers: { i18n: __ } });
}

115
Backend/src/views/views.ts Normal file
View File

@ -0,0 +1,115 @@
import {
IRouter,
Request,
RequestHandler,
Router,
static as ServeStatic,
} from "express";
import * as Handlebars from "handlebars";
import * as moment from "moment";
import { GetUserMiddleware, UserMiddleware } from "../api/middlewares/user";
import GetAuthRoute from "../api/oauth/auth";
import config from "../config";
import { HttpStatusCode } from "../helper/request_error";
import GetAdminPage from "./admin";
import GetRegistrationPage from "./register";
import * as path from "path";
const viewsv2_location = path.join(path.dirname(require.resolve("@hibas123/openauth-views-v2")), "build");
Handlebars.registerHelper("appname", () => config.core.name);
const cacheTime = !config.core.dev
? moment.duration(1, "month").asSeconds()
: 1000;
const addCache: RequestHandler = (req, res, next) => {
res.setHeader("cache-control", "public, max-age=" + cacheTime);
next();
};
const ViewRouter: IRouter = Router();
ViewRouter.get("/", UserMiddleware, (req, res) => {
res.send("This is the main page");
});
ViewRouter.get("/register", (req, res) => {
res.setHeader("Cache-Control", "public, max-age=" + cacheTime);
res.send(GetRegistrationPage(req.__));
});
ViewRouter.use(
"/login",
addCache,
ServeStatic(path.join(viewsv2_location, "login"), { cacheControl: false })
);
ViewRouter.use(
"/user",
addCache,
ServeStatic(path.join(viewsv2_location, "user"), { cacheControl: false })
);
ViewRouter.get("/code", (req, res) => {
res.setHeader("Cache-Control", "no-cache");
if (req.query.error) res.send("Some error occured: " + req.query.error);
else res.send(`Your code is: ${req.query.code}`);
});
ViewRouter.get(
"/admin",
GetUserMiddleware(false, true),
(req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN);
else next();
},
(req, res) => {
res.send(GetAdminPage(req.__));
}
);
ViewRouter.get("/auth", GetAuthRoute(true));
ViewRouter.use(
"/popup",
GetUserMiddleware(false, false),
addCache,
ServeStatic(path.join(viewsv2_location, "popup"), { cacheControl: false })
);
// ViewRouter.get("/popup", UserMiddleware, (req, res) => {
// res.send(GetPopupPage(req.__));
// });
// if (config.core.dev) {
// const logo =
// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAlwSFlzAAASdAAAEnQB3mYfeAAAAAZiS0dEAP8A/wD/oL2nkwAAAFR0RVh0Y29tbWVudABGaWxlIHNvdXJjZTogaHR0cHM6Ly9jb21tb25zLndpa2ltZWRpYS5vcmcvd2lraS9GaWxlOkdvb2dsZS1mYXZpY29uLTIwMTUucG5nLE0iZQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wOS0wMlQxNzo1ODowOCswMDowMNkAVU4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDktMDJUMTc6NTg6MDgrMDA6MDCoXe3yAAAAR3RFWHRzb2Z0d2FyZQBJbWFnZU1hZ2ljayA2LjcuNy0xMCAyMDE0LTAzLTA2IFExNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9yZ2+foqIAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6aGVpZ2h0ADQwMUEAWp0AAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMzk0OUtQUwAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNDQxMjE2Njg4mYj7RAAAABN0RVh0VGh1bWI6OlNpemUAMTcuN0tCQhK/wrgAAAAzdEVYdFRodW1iOjpVUkkAZmlsZTovLy90bXAvbG9jYWxjb3B5XzFmZGViZjk0YmZkZC0xLnBuZ8EhOmkAAAkUSURBVGhDzZkLcBXVGcf/u/f9yM07gDq06Rh5BZTAtKMVwQGqaFEZmNLWgoBO1RRFausjbR1qKR0rHUDFtwVtqWPHgc4UmBq1NASUiFQGwiskMqZMSW6Tm+Q+knvvvvp9u0uamNxXctO5P9i5u+ec3T3/c77zfd/ZCBqBMUANhaB2+Ok3CMgSYLVC9ORBLC2F6CswW2WPrAmJHa5D9EAtpGMNkC+2AvE4IIrGcRlV1Q/BRqLGXwn7zNlwzF0A54JbqVIw2oyQUQmRms4g8voLiB14H5qiQHS5AJsNsFghCIk7pr+S2kOSoMWi+r2OG+fBc99a2K+tMltlxoiESKdPIvirGvo9ATHPB9gdEAaOfIboXYjHoAaDsF59DXy/+A3s180ya9MjIyGaLKP70QdoBmohFhSRidDoJxn5jKGuaLSe1K4uMrn5KNj6GgSLxaxMTtpCYh/VoXv9/fRgK0AmlMx0Rg0LivbpA1e4fQfss683KxKTlj2EXtiMrgdWQHB76HCPrQiGni+46D30vsDq7yD00hazIjEphfTUPILIGy+S2xyX9jRnDV53Ah0SecAUJDWtnifXIVq7j9ZD4cjWApuIecroT0jzOdwt9T9+eH/8M3hX/dAsTUxCIcFtz6D3zVdgKSzOSITGsYJiiEZeCGTjPKoamwq/hutoVgXycrDbE86wIaIdeY9vgOfuNWZpcoYVEj10AN3V9xjmlO4IUqe1cAiC0wX79XNg/+Zc2KZUQiwuheBwQCPz0Do7IDWfI8dxEPFD/4AW7IFA7lugqH8ZXYSfRPx8IzzLV5qlqRkihIOT/+uTIOQXpBUb+HbukFhUDO9Dj8H17SVmTWqif38Pod9thNrepr+PUf1t8G34LdxLv6dfp8sQIYEHV0I+8U/da6SCRauBThLwE3gpKo+UMGUH4eeeJRUKfL/eCvedy8ya9BkkJHa0EV2rbqc8qCylSWlmelH89l9hLb/aLB05sSOHoHxxAe7lK8ySzBgkJL7/K+jZ5oMa9kJ0Ui6UQIu+HkhE6YdHIZKvzwX6F4EapE6JrSj6ZSMc0wNQQjb2nkNgr6R2B1C6rz5nRDD9QrTmGmh0pUWs8N3bBN/3m2kRkxiFpsUUxJOnBjpQsJkCJC3uXKLftKT95Ovt5Nc5klLPBZcCpdNJplYJNSqSC+X4EIO1YhKKfv9nviWn0GdE7dhrrAddBCNA66PNjy+Ooqc/hb0iSDHCSibVjfxNW802uYXec82/e4CRDUCmqBy1wFd9Bu7bz8M27RuwjL/CrMwtdNOS6ydSh/9FE0L7i2Eh6xNliJN3QJywyixLn4ZmGR80ynDY0ssS0oUzHq8TqF5IGzsWIr1H64Pznn7T+hK0jLSYDNuiPhJEd2bIm/VxbN4bo5dmWQiNb1zW8NmmPDIojeIF/TcWyfBo1EBwUj40AhGM1SLA5RDgzvLhoYPF6AajSQHjbckiOTs29yTzInfgLnPXuno1mhE5bBYngVXby4zzHIPFRKIsJM00nUK6eZJb0OqGQtMiwuI1RjwZrFVqN85zDN6DWmgyRMHGO0AuSaKGvJkWOWde5Bb68iVvaJiWvuNMLISbIU71csgsyQx+GX9YlBUtoyPZ2DJ6PXW/wGXGc8H7NSrUffDwUGMbie299I5ZkBkOirMFlCjnu4W0j5I8SpOop8nEsFk5rTTMov5NgCScuR9q66u0dx4a2S1QybIUPNNdiX3iPBy883mzZuyZWRNCoSexP5Jo1kq8Anav95gzUkZbyy9NCA+EFTJNhoalnbfg1eh0XAg0oqWn1Wgwxnx4ilIiHukEIhiZ+nztRONLjC5ELF5onPW7WA02QcJ5uRCV/mU4K+ejUKBM2OHDw3UbzTZjy866ONx28yIBMQm44RrjC4wuhBEn3EE6FCpQYaUE8Y3IVNzceRsJUuEWKEXhdSJacTrQgj81Udo/hjRdUnH8C0Vfl4ng9cN51vxKo1H/xkqLnIZQNw2gdGpl13zUx8ahWIwNmVpu3t7bgYPLdqHcd5VZml0WboogTvbPOVoiWMRXS0T8odr42tM/I4JnKlq812Fm22J8Gi9BsWWoCIa/rpS6irDgL6vRFukwS7PH2p19CMWSi2DCUeCem/7nnPqFMN3T96MtGiRTkvQYmQiLSJstmxc3vPtdfNJ+wiwdPeveiuJIs6JntclQKOXl9bOgMoGQWYUTsKR8Hnpl2nekwEpiCmnxL9v/MJ468pxZOjJawmdx6/ZjaDjnRL4ruQgmSN1bv2iwJ+hfIwOZ/MdFcFuc+singm8PS72wW2z40Yy7sXrKEjpP4W5MzgY+x5bjO/G31noUuDV4Ox6Eq2M5NAv1lJzMcHDEd1AQrH1y8KeoYYUc85/CXXurMc5doq+JdFBo3xmRe3VhVWXTMOeKWZhRMgnjXCVwWR2IqRK6oj16HOLnf9T2Gf4d9sNjc8PJwuk1mqUHtvBseC6yi6duiYP/LsLP9gc17HnEjYoJgwd5WCHMK43vYNPRl/WFnclfqFR6nESdjisS/crUHY36yGHVeI1VsOizZxNtlLVy1B74bGol9kFQ3PC0PgtrrJyuI7pIJhDWcO9cG9be4jAKBpBQCLOh4XnsPLMHJc7CjMQMZODj030GpYw0O2G429bBEVhM570IRVVUlYt4ec3wH9eTCmGe/mQ7Xj/1LsoynJnRolFgZlOzB2+C8vkTqBivYdfaxN8MUgphdpzejacattHMFOne6v8Fd61TuoQ5xXfgrdueMEuHJy0hzMmOJvzg/Z8iJsfhpQU61rPD66sz2oWHZqzAY7PuM0sTk7aQyzx+eDPeplwr354HB3mbbAtSKN/riYd1U35t/kZUFleYNcnJWAhzMdyGmo+34MDFBuTZPOQ+HRRzBsXWjOAu8AyE4hHk2d14tGoNVk6+y6xNjxEJuYy/txPbT+7CnpYPaBRDFERdlLFaaR1ZKWXgvcTQ2eLX8T+Ftgzc+ZgSpw2SpMeeNVOXYnH5zWbLzBiVkIGc6jyP2tbD+LjtOM51XdDNQ6POshgjjrAI45pn8CrveFSVTsGcK2fjWxNv1M10NGRNyHAEKJKzuXCA5FliJ5HvyNP3NdkF+C8dn/ikO2g+hwAAAABJRU5ErkJggg==";
// ViewRouter.get("/devauth", (req, res) => {
// res.send(
// GetAuthPage(req.__, "Test 05265", [
// {
// name: "Access Profile",
// description:
// "It allows the application to know who you are. Required for all applications. And a lot of more Text, because why not? This will not stop, till it is multiple lines long and maybe kill the layout, so keep reading as long as you like, but I promise it will get boring after some time. So this should be enougth.",
// logo: logo,
// },
// {
// name: "Test 1",
// description:
// "This is not an real permission. This is used just to verify the layout",
// logo: logo,
// },
// {
// name: "Test 2",
// description:
// "This is not an real permission. This is used just to verify the layout",
// logo: logo,
// },
// ])
// );
// });
// }
export default ViewRouter;

122
Backend/src/web.ts Normal file
View File

@ -0,0 +1,122 @@
import { WebConfig } from "./config";
import * as express from "express";
import { Express } from "express";
import Logging from "@hibas123/nodelogging";
import * as bodyparser from "body-parser";
import * as cookieparser from "cookie-parser";
import * as i18n from "i18n";
import * as compression from "compression";
import ApiRouter from "./api";
import ViewRouter from "./views/views";
import RequestError, { HttpStatusCode } from "./helper/request_error";
export default class Web {
server: Express;
private port: number;
constructor(config: WebConfig) {
this.server = express();
this.port = Number(config.port);
this.registerMiddleware();
this.registerEndpoints();
this.registerErrorHandler();
}
listen() {
this.server.listen(this.port, () => {
Logging.log(`Server listening on port ${this.port}`);
});
}
private registerMiddleware() {
this.server.use(cookieparser());
this.server.use(
bodyparser.json(),
bodyparser.urlencoded({ extended: true })
);
this.server.use(i18n.init);
//Logging Middleware
this.server.use((req, res, next) => {
let start = process.hrtime();
let finished = false;
let to = false;
let listener = () => {
if (finished) return;
finished = true;
let td = process.hrtime(start);
let time = !to ? (td[0] * 1e3 + td[1] / 1e6).toFixed(2) : "--.--";
let resColor = "";
if (res.statusCode >= 200 && res.statusCode < 300)
resColor = "\x1b[32m";
//Green
else if (res.statusCode === 304 || res.statusCode === 302)
resColor = "\x1b[33m";
else if (res.statusCode >= 400 && res.statusCode < 500)
resColor = "\x1b[36m";
//Cyan
else if (res.statusCode >= 500 && res.statusCode < 600)
resColor = "\x1b[31m"; //Red
let m = req.method;
while (m.length < 4) m += " ";
Logging.log(
`${m} ${req.originalUrl} ${(req as any).language || ""
} ${resColor}${res.statusCode}\x1b[0m - ${time}ms`
);
res.removeListener("finish", listener);
};
res.on("finish", listener);
setTimeout(() => {
to = true;
listener();
}, 2000);
next();
});
this.server.use(
compression({
filter: (req, res) => {
if (req.headers["x-no-compression"]) {
return false;
}
return compression.filter(req, res);
},
})
);
}
private registerEndpoints() {
this.server.use("/api", ApiRouter);
this.server.use("/", ViewRouter);
}
private registerErrorHandler() {
this.server.use((error, req: express.Request, res, next) => {
if (!(error instanceof RequestError)) {
error = new RequestError(
error.message,
error.status || HttpStatusCode.INTERNAL_SERVER_ERROR,
error.nolog || false
);
}
if (error.status === 500 && !(<any>error).nolog) {
Logging.error(error);
} else {
Logging.log("Responded with Error", error.status, error.message);
}
if (req.accepts(["json"])) {
res.json_status = error.status || 500;
res.json({
error: error.message,
status: error.status || 500,
additional: error.additional,
});
} else res.status(error.status || 500).send(error.message);
});
}
}

17
Backend/tsconfig.json Normal file
View File

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

View File

@ -1,33 +1,31 @@
FROM node:12
FROM node:18-alpine
LABEL maintainer="Fabian Stamm <dev@fabianstamm.de>"
# RUN apt-get update
# # for https
# RUN apt-get install -yyq ca-certificates
# # install libraries
# RUN apt-get install -yyq libappindicator1 libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6
# # tools
# RUN apt-get install -yyq gconf-service lsb-release wget xdg-utils
# # and fonts
# RUN apt-get install -yyq fonts-liberation
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json", "tsconfig.json", "/usr/src/app/"]
# COPY ["package.json", "yarn.lock", ".yarnrc.yml", "/usr/src/app/"]
# COPY .yarn /usr/src/app/.yarn
# COPY Backend /usr/src/app/Backend
# COPY Frontend /usr/src/app/Frontend
# COPY FrontendLegacy /usr/src/app/FrontendLegacy
ENV NODE_ENV=production
COPY . /usr/src/app
RUN npm install
# RUN rm -rf /usr/src/app/Backend/node_modules &&\
# rm -rf /usr/src/app/Frontend/node_modules &&\
# rm -rf /usr/src/app/FrontendLegacy/node_modules &&\
# rm -rf /usr/src/app/Backend/logs &&\
# rm -rf /usr/src/app/Backend/keys
COPY lib/ /usr/src/app/lib
COPY views/out /usr/src/app/views/out/
COPY views_repo/build /usr/src/app/views_repo/build
RUN yarn install
RUN yarn build
RUN ln -s /usr/src/app/logs /usr/src/app/Backend/logs && ln -s /usr/src/app/keys /usr/src/app/Backend/keys
VOLUME [ "/usr/src/app/logs", "/usr/src/app/keys"]
EXPOSE 3004/tcp
WORKDIR /usr/src/app/Backend
CMD ["npm", "run", "start"]

11
Frontend/.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root = true
[*]
indent_style = space
indent_size = 3
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true
[*.svelte]
indent_size = 2

8
Frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
node_modules
public/bundle.*
yarn.lock
.rpt2_cache
build/
build.js
*.old

68
Frontend/README.md Normal file
View File

@ -0,0 +1,68 @@
_Psst <14>looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)_
---
# svelte app
This is a project template for [Svelte](https://svelte.technology) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npm install -g degit # you only need to do this once
degit sveltejs/template svelte-app
cd svelte-app
```
_Note that you will need to have [Node.js](https://nodejs.org) installed._
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
## Deploying to the web
### With [now](https://zeit.co/now)
Install `now` if you haven't already:
```bash
npm install -g now
```
Then, from within your project folder:
```bash
now
```
As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public
```

36
Frontend/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "@hibas123/openauth-views-v2",
"main": "index.js",
"devDependencies": {
"@rollup/plugin-html": "^1.0.2",
"@rollup/plugin-image": "^3.0.2",
"@rollup/plugin-node-resolve": "^15.0.2",
"@tsconfig/svelte": "^4.0.1",
"@types/cleave.js": "^1.4.7",
"esbuild": "^0.17.15",
"postcss": "^8.4.21",
"postcss-import": "^15.1.0",
"postcss-url": "^10.1.3",
"rollup": "^3.20.2",
"rollup-plugin-esbuild": "^5.0.0",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-sizes": "^1.0.5",
"rollup-plugin-svelte": "^7.1.4",
"rollup-plugin-visualizer": "^5.9.0",
"svelte": "^3.58.0",
"svelte-preprocess": "^5.0.3",
"typescript": "^5.0.3"
},
"scripts": {
"prepublishOnly": "npm run build",
"build": "rollup -c rollup.config.mjs ",
"dev": "rollup -c rollup.config.mjs -w"
},
"dependencies": {
"@hibas123/theme": "^2.0.6",
"@hibas123/utils": "^2.2.18",
"cleave.js": "^1.6.0",
"what-the-pack": "^2.0.3"
}
}

View File

@ -0,0 +1,3 @@
module.exports = {
plugins: [],
};

123
Frontend/rollup.config.mjs Normal file
View File

@ -0,0 +1,123 @@
import svelte from "rollup-plugin-svelte";
import esbuild from "rollup-plugin-esbuild";
import html from "@rollup/plugin-html";
import resolve from "@rollup/plugin-node-resolve";
import image from "@rollup/plugin-image";
import sizes from "rollup-plugin-sizes";
import { visualizer } from "rollup-plugin-visualizer";
import postcss from "rollup-plugin-postcss";
import livereload from "rollup-plugin-livereload";
import sveltePreprocess from "svelte-preprocess";
const VIEWS = ["home", "login", "popup", "user"];
const dev = process.env.NODE_ENV !== "production";
const htmlTemplate = ({ attributes, meta, files, publicPath, title }) => {
const makeHtmlAttributes = (attributes) => {
if (!attributes) {
return "";
}
const keys = Object.keys(attributes);
// eslint-disable-next-line no-param-reassign
return keys.reduce(
(result, key) => (result += ` ${key}="${attributes[key]}"`),
""
);
};
const scripts = (files.js || [])
.map(({ fileName }) => {
const attrs = makeHtmlAttributes(attributes.script);
return `<script src="${publicPath}${fileName}"${attrs}></script>`;
})
.join("\n");
const links = (files.css || [])
.map(({ fileName }) => {
const attrs = makeHtmlAttributes(attributes.link);
return `<link href="${publicPath}${fileName}" rel="stylesheet"${attrs}>`;
})
.join("\n");
const metas = meta
.map((input) => {
const attrs = makeHtmlAttributes(input);
return `<meta${attrs}>`;
})
.join("\n");
return `
<!doctype html>
<html${makeHtmlAttributes(attributes.html)}>
<head>
${metas}
<title>${title}</title>
<link rel="stylesheet" href="bundle.css"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto"/>
${links}
</head>
<body>
${scripts}
</body>
</html>`;
};
export default VIEWS.map((view) => ({
input: `src/pages/${view}/main.ts`,
output: [
dev
? {
file: `build/${view}/bundle.js`,
format: "iife",
sourcemap: true,
name: view,
}
: {
file: `build/${view}/bundle.min.js`,
format: "iife",
name: view,
plugins: [
esbuild({
minify: true,
}),
],
},
],
plugins: [
svelte({
emitCss: true,
preprocess: sveltePreprocess({}),
}),
esbuild({ sourceMap: dev }),
html({
title: view,
attributes: {
html: { lang: "en" },
},
meta: [
{
name: "viewport",
content: "width=device-width",
},
],
template: htmlTemplate,
}),
resolve({
browser: true,
dedupe: ["svelte"],
exportConditions: ["svelte"],
}),
image(),
sizes(),
visualizer({
filename: `build/stats/${view}.html`,
title: `Rullup bundle for ${view}`,
}),
postcss({
extract: `bundle.css`, //TODO: Check if it should be enabled
// inject: true,
}),
// dev && livereload(),
],
}));

View File

@ -0,0 +1,90 @@
<script lang="ts">
// import { Tile } from "carbon-components-svelte";
export let title: string;
export let loading = false;
export let hide = false;
</script>
<style>
.wrapper {
min-height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
box-sizing: border-box;
}
.container {
border-radius: 4px;
position: relative;
padding-top: 2.5rem;
min-height: calc(100px + 2.5rem);
min-width: 100px;
margin-top: 3rem;
}
.title-container {
margin: -4.8rem auto 0 auto;
max-width: 250px;
background-color: var(--primary);
color: white;
border-radius: 4px;
/* padding: 5px 20px; */
}
.title-container > h1 {
font-size: 2rem;
line-height: 1;
}
.content-container {
padding: 2em;
margin: 0 auto;
max-width: 380px;
overflow: hidden;
}
.loading_container {
filter: blur(1px) opacity(50%);
}
.loader_container {
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
}
</style>
<div class="wrapper">
<div class="card-elevated container">
<!-- <div class="container card"> -->
<div class="card elv-8 title-container">
<h1 style="margin:0">{title}</h1>
</div>
{#if loading}
<div class="loader_container">
<div class="loader_box">
<div class="loader" />
</div>
</div>
{/if}
<div class="content-container" class:loading_container={loading}>
{#if !(loading && hide)}
<slot />
{/if}
</div>
<!-- </div> -->
</div>
</div>

View File

@ -0,0 +1,15 @@
<script lang="ts">
// import { onMount, afterUpdate, setContext } from "svelte";
// import { writable, derived } from "svelte/store";
// type Theme = "white" | "g10" | "g90" | "g100";
// export let persist: boolean = false;
// export let persistKey: string = "theme";
export let dark = false;
</script>
<div class={dark ? 'dark-theme' : 'light-theme'}>
<slot />
</div>

View File

@ -0,0 +1,42 @@
import "@hibas123/theme/out/base.css";
import "./theme.css";
import { default as Theme } from "./Theme.svelte";
(() => {
const elements = new WeakSet();
function check() {
document
.querySelectorAll(".floating>input")
.forEach((e: HTMLInputElement) => {
if (elements.has(e)) return;
elements.add(e);
function checkState() {
console.log("Check State");
if (e.value !== "") {
if (e.classList.contains("used")) return;
e.classList.add("used");
} else {
if (e.classList.contains("used")) e.classList.remove("used");
}
}
e.addEventListener("change", () => checkState());
checkState();
});
}
const observer = new MutationObserver((mutations) => {
check();
});
// Start observing the target node for configured mutations
observer.observe(window.document, {
childList: true,
subtree: true,
});
check();
})();
export default Theme;

View File

@ -0,0 +1,251 @@
:root {
--primary: #1e88e5;
--mdc-theme-primary: var(--primary);
--mdc-theme-primary-bg: var(--mdc-theme--primary);
--mdc-theme-on-primary: white;
--error: #ff2f00;
--border-color: #ababab;
--default-font-size: 1.05rem;
}
* {
font-family: "Roboto", "Helvetica", sans-serif;
}
html,
body {
margin: 0;
color: #636363;
position: relative;
background: #eee;
height: 100%;
font-size: var(--default-font-size);
min-width: 100vw;
min-height: 100vh;
box-sizing: border-box;
}
.group {
position: relative;
margin-bottom: 24px;
min-height: 45px;
}
.floating > input {
font-size: 1.2rem;
padding: 10px 10px 10px 5px;
appearance: none;
-webkit-appearance: none;
display: block;
background: #fafafa;
background: unset;
color: #636363;
width: 100%;
border: none;
border-radius: 0;
/* border-bottom: 1px solid #757575; */
border-bottom: 1px solid var(--border-color);
box-sizing: border-box;
}
.floating > input:focus {
outline: none;
}
/* Label */
.floating > label {
color: #999;
font-size: 18px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 5px;
top: 10px;
transition: all 0.2s ease;
}
/* active */
.floating > input:focus ~ label,
.floating > input.used ~ label {
top: -0.75em;
transform: scale(0.75);
left: -2px;
/* font-size: 14px; */
color: var(--primary);
transform-origin: left;
}
/* Underline */
.bar {
position: relative;
display: block;
width: 100%;
}
.bar:before,
.bar:after {
content: "";
height: 2px;
width: 0;
bottom: 1px;
position: absolute;
background: var(--primary);
transition: all 0.2s ease;
}
.bar:before {
left: 50%;
}
.bar:after {
right: 50%;
}
/* active */
.floating > input:focus ~ .bar:before,
.floating > input:focus ~ .bar:after {
width: 50%;
}
/* Highlight */
.highlight {
position: absolute;
height: 60%;
width: 100px;
top: 25%;
left: 0;
pointer-events: none;
opacity: 0.5;
}
/* active */
.floating > input:focus ~ .highlight {
animation: inputHighlighter 0.3s ease;
}
/* Animations */
@keyframes inputHighlighter {
from {
background: var(--primary);
}
to {
width: 0;
background: transparent;
}
}
.btn {
position: relative;
display: block;
margin: 2rem;
padding: 0 1em;
overflow: hidden;
border-width: 0;
outline: none;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
background-color: #cccccc;
color: #ecf0f1;
transition: background-color 0.3s;
height: 48px;
text-transform: uppercase;
font-weight: 500;
font-size: 1.2rem;
}
.btn:hover,
.btn:focus {
filter: brightness(90%);
}
.btn > * {
position: relative;
}
.btn span {
display: block;
padding: 12px 24px;
}
.btn:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 0;
padding-top: 0;
border-radius: 100%;
background-color: rgba(236, 240, 241, 0.3);
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.btn:active:before {
width: 120%;
padding-top: 120%;
transition: width 0.2s ease-out, padding-top 0.2s ease-out;
}
.loader_box {
width: 64px;
height: 64px;
margin: auto;
}
.loader {
display: inline-block;
position: relative;
z-index: 100;
}
.loader:after {
content: " ";
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid var(--primary);
border-color: var(--primary) transparent var(--primary) transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#content {
height: 100%;
}

View File

@ -0,0 +1,20 @@
export function setCookie(cname: string, cvalue: string, exdate: string) {
const expires = exdate ? `;expires=${exdate}` : "";
document.cookie = `${cname}=${cvalue}${expires};path=/;`;
}
export function getCookie(cname: string) {
const name = cname + "=";
const dc = decodeURIComponent(document.cookie);
const ca = dc.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

View File

@ -0,0 +1,53 @@
import { getCookie } from "./cookie";
const baseURL = "";
export default async function request(
endpoint: string,
parameters: { [key: string]: string } = {},
method: "GET" | "POST" | "DELETE" | "PUT" = "GET",
body?: any,
authInParam = false,
redirect = false
) {
let pairs = [];
if (authInParam) {
parameters.login = getCookie("login");
parameters.special = getCookie("special");
}
for (let key in parameters) {
pairs.push(key + "=" + parameters[key]);
}
let url = endpoint;
if (pairs.length > 0) {
url += "?" + pairs.join("&");
}
return fetch(baseURL + url, {
method,
body: JSON.stringify(body),
credentials: "same-origin",
headers: {
"content-type": "application/json",
},
})
.then((e) => {
if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) {
if (redirect && data.additional && data.additional.auth) {
let state = btoa(
window.location.pathname + window.location.hash
);
window.location.href = `/login?state=${state}&base64=true`;
}
return Promise.reject(new Error(data.error));
}
return data;
});
}

View File

@ -0,0 +1,484 @@
var b;
if (!(b = t)) {
var w = Math,
y = {},
B = (y.p = {}),
aa = function () {},
C = (B.A = {
extend: function (o) {
aa.prototype = this;
var _ = new aa();
return o && _.u(o), (_.z = this), _;
},
create: function () {
var o = this.extend();
return o.h.apply(o, arguments), o;
},
h: function () {},
u: function (o) {
for (var _ in o) o.hasOwnProperty(_) && (this[_] = o[_]);
o.hasOwnProperty("toString") && (this.toString = o.toString);
},
e: function () {
return this.z.extend(this);
},
}),
D = (B.i = C.extend({
h: function (o, _) {
(o = this.d = o || []), (this.c = void 0 == _ ? 4 * o.length : _);
},
toString: function (o) {
return (o || ba).stringify(this);
},
concat: function (o) {
var _ = this.d,
Da = o.d,
Ea = this.c,
o = o.c;
if ((this.t(), Ea % 4))
for (var Fa = 0; Fa < o; Fa++)
_[(Ea + Fa) >>> 2] |=
(255 & (Da[Fa >>> 2] >>> (24 - 8 * (Fa % 4)))) <<
(24 - 8 * ((Ea + Fa) % 4));
else if (65535 < Da.length)
for (Fa = 0; Fa < o; Fa += 4) _[(Ea + Fa) >>> 2] = Da[Fa >>> 2];
else _.push.apply(_, Da);
return (this.c += o), this;
},
t: function () {
var o = this.d,
_ = this.c;
(o[_ >>> 2] &= 4294967295 << (32 - 8 * (_ % 4))),
(o.length = w.ceil(_ / 4));
},
e: function () {
var o = C.e.call(this);
return (o.d = this.d.slice(0)), o;
},
random: function (o) {
for (var _ = [], Da = 0; Da < o; Da += 4)
_.push(0 | (4294967296 * w.random()));
return D.create(_, o);
},
})),
ca = (y.O = {}),
ba = (ca.K = {
stringify: function (o) {
for (var Fa, _ = o.d, o = o.c, Da = [], Ea = 0; Ea < o; Ea++)
(Fa = 255 & (_[Ea >>> 2] >>> (24 - 8 * (Ea % 4)))),
Da.push((Fa >>> 4).toString(16)),
Da.push((15 & Fa).toString(16));
return Da.join("");
},
parse: function (o) {
for (var _ = o.length, Da = [], Ea = 0; Ea < _; Ea += 2)
Da[Ea >>> 3] |=
parseInt(o.substr(Ea, 2), 16) << (24 - 4 * (Ea % 8));
return D.create(Da, _ / 2);
},
}),
da = (ca.M = {
stringify: function (o) {
for (var _ = o.d, o = o.c, Da = [], Ea = 0; Ea < o; Ea++)
Da.push(
String.fromCharCode(
255 & (_[Ea >>> 2] >>> (24 - 8 * (Ea % 4)))
)
);
return Da.join("");
},
parse: function (o) {
for (var _ = o.length, Da = [], Ea = 0; Ea < _; Ea++)
Da[Ea >>> 2] |= (255 & o.charCodeAt(Ea)) << (24 - 8 * (Ea % 4));
return D.create(Da, _);
},
}),
ea = (ca.N = {
stringify: function (o) {
try {
return decodeURIComponent(escape(da.stringify(o)));
} catch (_) {
throw Error("Malformed UTF-8 data");
}
},
parse: function (o) {
return da.parse(unescape(encodeURIComponent(o)));
},
}),
ia = (B.I = C.extend({
reset: function () {
(this.g = D.create()), (this.j = 0);
},
l: function (o) {
"string" == typeof o && (o = ea.parse(o)),
this.g.concat(o),
(this.j += o.c);
},
m: function (o) {
var _ = this.g,
Da = _.d,
Ea = _.c,
Fa = this.n,
Ga = Ea / (4 * Fa),
Ga = o ? w.ceil(Ga) : w.max((0 | Ga) - this.r, 0),
o = Ga * Fa,
Ea = w.min(4 * o, Ea);
if (o) {
for (var Ha = 0; Ha < o; Ha += Fa) this.H(Da, Ha);
(Ha = Da.splice(0, o)), (_.c -= Ea);
}
return D.create(Ha, Ea);
},
e: function () {
var o = C.e.call(this);
return (o.g = this.g.e()), o;
},
r: 0,
}));
B.B = ia.extend({
h: function () {
this.reset();
},
reset: function () {
ia.reset.call(this), this.q();
},
update: function (o) {
return this.l(o), this.m(), this;
},
o: function (o) {
return o && this.l(o), this.G(), this.f;
},
e: function () {
var o = ia.e.call(this);
return (o.f = this.f.e()), o;
},
n: 16,
D: function (o) {
return function (_, Da) {
return o.create(Da).o(_);
};
},
F: function (o) {
return function (_, Da) {
return ja.J.create(o, Da).o(_);
};
},
});
var ja = (y.s = {});
b = y;
}
var t = b,
K = t,
ka = K.p,
la = ka.A,
va = ka.i,
K = (K.w = {});
(K.C = la.extend({
h: function (o, _) {
(this.a = o), (this.b = _);
},
})),
(K.i = la.extend({
h: function (o, _) {
(o = this.d = o || []), (this.c = void 0 == _ ? 8 * o.length : _);
},
v: function () {
for (var Fa, o = this.d, _ = o.length, Da = [], Ea = 0; Ea < _; Ea++)
(Fa = o[Ea]), Da.push(Fa.a), Da.push(Fa.b);
return va.create(Da, this.c);
},
e: function () {
for (
var o = la.e.call(this),
_ = (o.d = this.d.slice(0)),
Da = _.length,
Ea = 0;
Ea < Da;
Ea++
)
_[Ea] = _[Ea].e();
return o;
},
}));
function L() {
return wa.create.apply(wa, arguments);
}
for (
var xa = t.p.B,
M = t.w,
wa = M.C,
ya = M.i,
M = t.s,
za = [
L(1116352408, 3609767458),
L(1899447441, 602891725),
L(3049323471, 3964484399),
L(3921009573, 2173295548),
L(961987163, 4081628472),
L(1508970993, 3053834265),
L(2453635748, 2937671579),
L(2870763221, 3664609560),
L(3624381080, 2734883394),
L(310598401, 1164996542),
L(607225278, 1323610764),
L(1426881987, 3590304994),
L(1925078388, 4068182383),
L(2162078206, 991336113),
L(2614888103, 633803317),
L(3248222580, 3479774868),
L(3835390401, 2666613458),
L(4022224774, 944711139),
L(264347078, 2341262773),
L(604807628, 2007800933),
L(770255983, 1495990901),
L(1249150122, 1856431235),
L(1555081692, 3175218132),
L(1996064986, 2198950837),
L(2554220882, 3999719339),
L(2821834349, 766784016),
L(2952996808, 2566594879),
L(3210313671, 3203337956),
L(3336571891, 1034457026),
L(3584528711, 2466948901),
L(113926993, 3758326383),
L(338241895, 168717936),
L(666307205, 1188179964),
L(773529912, 1546045734),
L(1294757372, 1522805485),
L(1396182291, 2643833823),
L(1695183700, 2343527390),
L(1986661051, 1014477480),
L(2177026350, 1206759142),
L(2456956037, 344077627),
L(2730485921, 1290863460),
L(2820302411, 3158454273),
L(3259730800, 3505952657),
L(3345764771, 106217008),
L(3516065817, 3606008344),
L(3600352804, 1432725776),
L(4094571909, 1467031594),
L(275423344, 851169720),
L(430227734, 3100823752),
L(506948616, 1363258195),
L(659060556, 3750685593),
L(883997877, 3785050280),
L(958139571, 3318307427),
L(1322822218, 3812723403),
L(1537002063, 2003034995),
L(1747873779, 3602036899),
L(1955562222, 1575990012),
L(2024104815, 1125592928),
L(2227730452, 2716904306),
L(2361852424, 442776044),
L(2428436474, 593698344),
L(2756734187, 3733110249),
L(3204031479, 2999351573),
L(3329325298, 3815920427),
L(3391569614, 3928383900),
L(3515267271, 566280711),
L(3940187606, 3454069534),
L(4118630271, 4000239992),
L(116418474, 1914138554),
L(174292421, 2731055270),
L(289380356, 3203993006),
L(460393269, 320620315),
L(685471733, 587496836),
L(852142971, 1086792851),
L(1017036298, 365543100),
L(1126000580, 2618297676),
L(1288033470, 3409855158),
L(1501505948, 4234509866),
L(1607167915, 987167468),
L(1816402316, 1246189591),
],
$ = [],
Aa = 0;
80 > Aa;
Aa++
)
$[Aa] = L();
(M = M.k = xa.extend({
q: function () {
this.f = ya.create([
L(1779033703, 4089235720),
L(3144134277, 2227873595),
L(1013904242, 4271175723),
L(2773480762, 1595750129),
L(1359893119, 2917565137),
L(2600822924, 725511199),
L(528734635, 4215389547),
L(1541459225, 327033209),
]);
},
H: function (o, _) {
for (
var qb,
Da = this.f.d,
Ea = Da[0],
Fa = Da[1],
Ga = Da[2],
Ha = Da[3],
Ia = Da[4],
Ja = Da[5],
Ka = Da[6],
Da = Da[7],
La = Ea.a,
Ma = Ea.b,
Na = Fa.a,
Oa = Fa.b,
Pa = Ga.a,
Qa = Ga.b,
Ra = Ha.a,
Sa = Ha.b,
Ta = Ia.a,
Ua = Ia.b,
Va = Ja.a,
Wa = Ja.b,
Xa = Ka.a,
Ya = Ka.b,
Za = Da.a,
$a = Da.b,
_a = La,
ab = Ma,
bb = Na,
cb = Oa,
db = Pa,
eb = Qa,
fb = Ra,
gb = Sa,
hb = Ta,
ib = Ua,
jb = Va,
kb = Wa,
lb = Xa,
mb = Ya,
nb = Za,
ob = $a,
pb = 0;
80 > pb;
pb++
) {
if (((qb = $[pb]), 16 > pb))
var rb = (qb.a = 0 | o[_ + 2 * pb]),
sb = (qb.b = 0 | o[_ + 2 * pb + 1]);
else {
var rb = $[pb - 15],
sb = rb.a,
tb = rb.b,
rb =
((tb << 31) | (sb >>> 1)) ^
((tb << 24) | (sb >>> 8)) ^
(sb >>> 7),
tb =
((sb << 31) | (tb >>> 1)) ^
((sb << 24) | (tb >>> 8)) ^
((sb << 25) | (tb >>> 7)),
ub = $[pb - 2],
sb = ub.a,
vb = ub.b,
ub =
((vb << 13) | (sb >>> 19)) ^
((sb << 3) | (vb >>> 29)) ^
(sb >>> 6),
vb =
((sb << 13) | (vb >>> 19)) ^
((vb << 3) | (sb >>> 29)) ^
((sb << 26) | (vb >>> 6)),
sb = $[pb - 7],
wb = sb.a,
xb = $[pb - 16],
yb = xb.a,
xb = xb.b,
sb = tb + sb.b,
rb = rb + wb + (sb >>> 0 < tb >>> 0 ? 1 : 0),
sb = sb + vb,
rb = rb + ub + (sb >>> 0 < vb >>> 0 ? 1 : 0),
sb = sb + xb,
rb = rb + yb + (sb >>> 0 < xb >>> 0 ? 1 : 0);
(qb.a = rb), (qb.b = sb);
}
var wb = (hb & jb) ^ (~hb & lb),
xb = (ib & kb) ^ (~ib & mb),
qb = (_a & bb) ^ (_a & db) ^ (bb & db),
tb =
((ab << 4) | (_a >>> 28)) ^
((_a << 30) | (ab >>> 2)) ^
((_a << 25) | (ab >>> 7)),
ub =
((_a << 4) | (ab >>> 28)) ^
((ab << 30) | (_a >>> 2)) ^
((ab << 25) | (_a >>> 7)),
vb = za[pb],
Ab = vb.a,
Bb = vb.b,
vb =
ob +
(((hb << 18) | (ib >>> 14)) ^
((hb << 14) | (ib >>> 18)) ^
((ib << 23) | (hb >>> 9))),
yb =
nb +
(((ib << 18) | (hb >>> 14)) ^
((ib << 14) | (hb >>> 18)) ^
((hb << 23) | (ib >>> 9))) +
(vb >>> 0 < ob >>> 0 ? 1 : 0),
vb = vb + xb,
yb = yb + wb + (vb >>> 0 < xb >>> 0 ? 1 : 0),
vb = vb + Bb,
yb = yb + Ab + (vb >>> 0 < Bb >>> 0 ? 1 : 0),
vb = vb + sb,
yb = yb + rb + (vb >>> 0 < sb >>> 0 ? 1 : 0),
sb = ub + ((ab & cb) ^ (ab & eb) ^ (cb & eb)),
qb = tb + qb + (sb >>> 0 < ub >>> 0 ? 1 : 0),
nb = lb,
ob = mb,
lb = jb,
mb = kb,
jb = hb,
kb = ib,
ib = 0 | (gb + vb),
hb = 0 | (fb + yb + (ib >>> 0 < gb >>> 0 ? 1 : 0)),
fb = db,
gb = eb,
db = bb,
eb = cb,
bb = _a,
cb = ab,
ab = 0 | (vb + sb),
_a = 0 | (yb + qb + (ab >>> 0 < vb >>> 0 ? 1 : 0));
}
(Ma = Ea.b = 0 | (Ma + ab)),
(Ea.a = 0 | (La + _a + (Ma >>> 0 < ab >>> 0 ? 1 : 0))),
(Oa = Fa.b = 0 | (Oa + cb)),
(Fa.a = 0 | (Na + bb + (Oa >>> 0 < cb >>> 0 ? 1 : 0))),
(Qa = Ga.b = 0 | (Qa + eb)),
(Ga.a = 0 | (Pa + db + (Qa >>> 0 < eb >>> 0 ? 1 : 0))),
(Sa = Ha.b = 0 | (Sa + gb)),
(Ha.a = 0 | (Ra + fb + (Sa >>> 0 < gb >>> 0 ? 1 : 0))),
(Ua = Ia.b = 0 | (Ua + ib)),
(Ia.a = 0 | (Ta + hb + (Ua >>> 0 < ib >>> 0 ? 1 : 0))),
(Wa = Ja.b = 0 | (Wa + kb)),
(Ja.a = 0 | (Va + jb + (Wa >>> 0 < kb >>> 0 ? 1 : 0))),
(Ya = Ka.b = 0 | (Ya + mb)),
(Ka.a = 0 | (Xa + lb + (Ya >>> 0 < mb >>> 0 ? 1 : 0))),
($a = Da.b = 0 | ($a + ob)),
(Da.a = 0 | (Za + nb + ($a >>> 0 < ob >>> 0 ? 1 : 0)));
},
G: function () {
var o = this.g,
_ = o.d,
Da = 8 * this.j,
Ea = 8 * o.c;
(_[Ea >>> 5] |= 128 << (24 - (Ea % 32))),
(_[(((Ea + 128) >>> 10) << 5) + 31] = Da),
(o.c = 4 * _.length),
this.m(),
(this.f = this.f.v());
},
n: 32,
})),
(t.k = xa.D(M)),
(t.L = xa.F(M));
export default function sha512(o) {
return t.k(o) + "";
}

View File

@ -0,0 +1,44 @@
<style>
.main {
padding: 2rem;
}
li {
list-style: none;
padding: 1rem;
}
li > a {
text-decoration: none;
}
</style>
<div class="main">
<h1>Home Page</h1>
<h2>About</h2>
<p>
OpenAuth is a Service to provide simple Authentication to a veriaty of
Applications. With a simple to use API and different Strategies, it can be
easily integrated into most Applications.
</p>
<h2>QickLinks</h2>
<p>
If you want to manage your Account, click
<a href="user.html">here</a>
</p>
<h2>Applications using OpenAuth</h2>
<ul>
<li>
<a href="https://ebook.stamm.me">EBook Store and Reader</a>
</li>
<li>
<a href="https://notes.hibas123.de">
Secure and Simple Notes application
</a>
</li>
</ul>
</div>

View File

@ -0,0 +1,8 @@
import "../../components/theme";
import App from "./App.svelte";
const app = new App({
target: document.body,
});
export default app;

View File

@ -0,0 +1,124 @@
<script>
import Theme from "../../components/theme";
import HoveringContentBox from "../../components/HoveringContentBox.svelte";
import Api from "./api.ts";
import Credentials from "./Credentials.svelte";
import Redirect from "./Redirect.svelte";
import Twofactor from "./Twofactor.svelte";
const appname = "OpenAuth";
const states = {
credentials: 1,
twofactor: 3,
redirect: 4,
};
let username = Api.getUsername();
let password = "";
let loading = false;
let state = states.credentials;
function getButtonText(state) {
switch (state) {
case states.username:
return "Next";
case states.password:
return "Login";
default:
return "";
}
}
$: btnText = getButtonText(state);
let error;
// window.addEventListener("popstate", () => {
// state = history.state;
// })
function LoadRedirect() {
state = states.redirect;
}
function Loading() {
state = states.loading;
}
let salt;
async function buttonClick() {
if (state === states.username) {
Loading();
let res = await Api.setUsername(username);
if (res.error) {
error = res.error;
LoadUsername();
} else {
LoadPassword();
}
} else if (state === states.password) {
Loading();
let res = await Api.setPassword(password);
if (res.error) {
error = res.error;
LoadPassword();
} else {
if (res.tfa) {
// TODO: Make TwoFactor UI/-s
} else {
LoadRedirect();
}
}
btnText = "Error";
}
}
function startRedirect() {
state = states.redirect;
// Show message to User and then redirect
setTimeout(() => Api.finish(), 2000);
}
function afterCredentials() {
Object.keys(Api); // Some weird bug needs this???
if (Api.twofactor) {
state = states.twofactor;
} else {
startRedirect();
}
}
function afterTwoFactor() {
startRedirect();
}
</script>
<style>
footer {
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
</style>
<Theme>
<HoveringContentBox title="Login" {loading}>
<form action="JavaScript:void(0)">
{#if state === states.redirect}
<Redirect />
{:else if state === states.credentials}
<Credentials next={afterCredentials} setLoading={(s) => (loading = s)} />
{:else if state === states.twofactor}
<Twofactor finish={afterTwoFactor} setLoading={(s) => (loading = s)} />
{/if}
</form>
</HoveringContentBox>
<footer>
<p>Powered by {appname}</p>
</footer>
</Theme>

View File

@ -0,0 +1,84 @@
<script>
import Api from "./api.ts";
let error;
let password = "";
let username = Api.getUsername();
const states = {
username: 1,
password: 2
};
let state = states.username;
let salt;
export let setLoading;
export let next;
async function buttonClick() {
setLoading(true);
if (state === states.username) {
let res = await Api.setUsername(username);
if (res.error) {
error = res.error;
} else {
state = states.password;
error = undefined;
}
} else if (state === states.password) {
let res = await Api.setPassword(password);
if (res.error) {
error = res.error;
} else {
error = undefined;
next();
}
}
setLoading(false);
}
</script>
<style>
.error {
color: var(--error);
padding: 4px;
}
.wide-button {
width: 100%;
margin: 0;
}
</style>
{#if state === states.username}
<h3>Enter your Username or your E-Mail Address</h3>
<div class="floating group">
<input
type="text"
autocomplete="username"
autofocus
bind:value={username} />
<span class="highlight" />
<span class="bar" />
<label>Username or E-Mail</label>
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
</div>
{:else}
<h3>Enter password for {username}</h3>
<div class="floating group">
<input
type="password"
autocomplete="password"
autofocus
bind:value={password} />
<span class="highlight" />
<span class="bar" />
<label>Password</label>
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
</div>
{/if}
<button class="btn btn-primary wide-button" on:click={buttonClick}>Next</button>

View File

@ -0,0 +1,99 @@
<script>
// import {
// onMount,
// onDestroy
// } from "svelte";
import { onMount, onDestroy } from "svelte";
const basetext = "Logged in. Redirecting";
let dots = 0;
$: text = basetext + ".".repeat(dots);
let iv;
onMount(() => {
console.log("Mounted");
iv = setInterval(() => {
dots++;
if (dots > 3) dots = 0;
}, 500);
});
onDestroy(() => {
console.log("on Destroy");
clearInterval(iv);
});
</script>
<style>
.checkmark__circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #7ac142;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark {
width: 56px;
height: 56px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #fff;
stroke-miterlimit: 10;
margin: 10% auto;
box-shadow: inset 0px 0px 0px #7ac142;
animation: fill 0.4s ease-in-out 0.4s forwards,
scale 0.3s ease-in-out 0.9s both;
}
.checkmark__check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
@keyframes stroke {
100% {
stroke-dashoffset: 0;
}
}
@keyframes scale {
0%,
100% {
transform: none;
}
50% {
transform: scale3d(1.1, 1.1, 1);
}
}
@keyframes fill {
100% {
box-shadow: inset 0px 0px 0px 30px #7ac142;
}
}
.scale {
transform: scale(1.5);
}
</style>
<div class="scale">
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none" />
<path
class="checkmark__check"
fill="none"
d="M14.1 27.2l7.1 7.2 16.7-16.8" />
</svg>
</div>
<!-- <div style="text-align: center;"> -->
<h3>{text}</h3>
<!-- </div> -->

View File

@ -0,0 +1,104 @@
<script>
import Api, { TFATypes } from "./api.ts";
import Icon from "./icons/Icon.svelte";
import OTCTwoFactor from "./twofactors/otc.svelte";
import PushTwoFactor from "./twofactors/push.svelte";
import U2FTwoFactor from "./twofactors/u2f.svelte";
const states = {
list: 1,
twofactor: 2
};
function getIcon(tf) {
switch (tf.type) {
case TFATypes.OTC:
return "Authenticator";
case TFATypes.BACKUP_CODE:
return "BackupCode";
case TFATypes.U2F:
return "SecurityKey";
case TFATypes.APP_ALLOW:
return "AppPush";
}
}
let twofactors = Api.twofactor.map(tf => {
return {
...tf,
icon: getIcon(tf)
};
});
let state = states.list;
let twofactor = undefined;
twofactor = twofactors[0];
$: console.log(twofactor);
function onFinish(res) {
if (res) finish();
else twofactor = undefined;
}
export let finish;
</script>
<style>
ul {
list-style: none;
padding-inline-start: 0;
margin-bottom: 0;
}
li {
border-top: 1px grey solid;
padding: 1em;
cursor: pointer;
}
li:first-child {
border-top: none !important;
}
.icon {
float: left;
height: 24px;
width: 24px;
}
.name {
margin-left: 48px;
line-height: 24px;
font-size: 20px;
}
</style>
<div>
{#if !twofactor}
<h3>Select your Authentication method:</h3>
<ul>
{#each twofactors as tf}
<li on:click={() => (twofactor = tf)}>
<div class="icon">
<Icon icon_name={tf.icon} />
</div>
<div class="name">{tf.name}</div>
</li>
{/each}
</ul>
{:else if twofactor.type === TFATypes.OTC}
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={true} />
{:else if twofactor.type === TFATypes.BACKUP_CODE}
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={false} />
{:else if twofactor.type === TFATypes.U2F}
<U2FTwoFactor id={twofactor.id} finish={onFinish} />
{:else if twofactor.type === TFATypes.APP_ALLOW}
<PushTwoFactor id={twofactor.id} finish={onFinish} />
{:else}
<div>Invalid TwoFactor Method!</div>
{/if}
</div>

View File

@ -0,0 +1,182 @@
import request from "../../helper/request";
import sha from "../../helper/sha512";
import { setCookie, getCookie } from "../../helper/cookie";
export interface TwoFactor {
id: string;
name: string;
type: TFATypes;
}
export enum TFATypes {
OTC,
BACKUP_CODE,
U2F,
APP_ALLOW,
}
// const Api = {
// // twofactor: [{
// // id: "1",
// // name: "Backup Codes",
// // type: TFATypes.BACKUP_CODE
// // }, {
// // id: "2",
// // name: "YubiKey",
// // type: TFATypes.U2F
// // }, {
// // id: "3",
// // name: "Authenticator",
// // type: TFATypes.OTC
// // }] as TwoFactor[],
// }
export interface IToken {
token: string;
expires: string;
}
function makeid(length) {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export default class Api {
static salt: string;
static login: IToken;
static special: IToken;
static username: string;
static twofactor: any[];
static getUsername() {
return this.username || getCookie("username");
}
static async setUsername(
username: string
): Promise<{ error: string | undefined }> {
return request(
"/api/user/login",
{
type: "username",
username,
},
"POST"
)
.then((res) => {
this.salt = res.salt;
this.username = username;
return {
error: undefined,
};
})
.catch((err) => {
let error = err.message;
return { error };
});
}
static async setPassword(
password: string
): Promise<{ error: string | undefined; twofactor?: any }> {
const date = new Date().valueOf();
let pw = sha(sha(this.salt + password) + date.toString());
return request(
"/api/user/login",
{
type: "password",
},
"POST",
{
username: this.username,
password: pw,
date,
}
)
.then(({ login, special, tfa }) => {
this.login = login;
this.special = special;
if (tfa && Array.isArray(tfa) && tfa.length > 0)
this.twofactor = tfa;
else this.twofactor = undefined;
return {
error: undefined,
};
})
.catch((err) => {
let error = err.message;
return { error };
});
}
static gettok() {
return {
login: this.login.token,
special: this.special.token,
};
}
static async sendBackup(id: string, code: string) {
return request("/api/user/twofactor/backup", this.gettok(), "PUT", {
code,
id,
})
.then(({ login_exp, special_exp }) => {
this.login.expires = login_exp;
this.special.expires = special_exp;
return {};
})
.catch((err) => ({ error: err.message }));
}
static async sendOTC(id: string, code: string) {
return request("/api/user/twofactor/otc", this.gettok(), "PUT", {
code,
id,
})
.then(({ login_exp, special_exp }) => {
this.login.expires = login_exp;
this.special.expires = special_exp;
return {};
})
.catch((error) => ({ error: error.message }));
}
static finish() {
let d = new Date();
d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
setCookie("username", this.username, d.toUTCString());
setCookie(
"login",
this.login.token,
new Date(this.login.expires).toUTCString()
);
setCookie(
"special",
this.special.token,
new Date(this.special.expires).toUTCString()
);
let url = new URL(window.location.href);
let state = url.searchParams.get("state");
let red = "/";
if (state) {
let base64 = url.searchParams.get("base64");
if (base64) red = atob(state);
else red = state;
}
setTimeout(() => (window.location.href = red), 200);
}
}

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M18.617,1.72c0,-0.949 -0.771,-1.72 -1.721,-1.72l-9.792,0c-0.95,0 -1.721,0.771 -1.721,1.72l0,20.56c0,0.949 0.771,1.72 1.721,1.72l9.792,0c0.95,0 1.721,-0.771 1.721,-1.72l0,-20.56Z" style="fill:#4d4d4d;"/><rect x="6" y="3" width="12" height="18" style="fill:#b3b3b3;"/><path d="M14,1.5c0,-0.129 -0.105,-0.233 -0.233,-0.233l-3.534,0c-0.128,0 -0.233,0.104 -0.233,0.233c0,0.129 0.105,0.233 0.233,0.233l3.534,0c0.128,0 0.233,-0.104 0.233,-0.233Z" style="fill:#b3b3b3;"/><ellipse cx="12" cy="22.5" rx="0.983" ry="1" style="fill:#b3b3b3;"/></svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><path d="M18.5,12c0,3.59 -2.91,6.5 -6.5,6.5c-3.59,0 -6.5,-2.91 -6.5,-6.5c0,-3.59 2.91,-6.5 6.5,-6.5c1.729,0 3.295,0.679 4.46,1.78l4.169,-3.599c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5.5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle id="XMLID_1331_" cx="12" cy="12" r="12" style="fill:#808080;"/><path d="M19,12c0,3.866 -3.134,7 -7,7c-3.866,0 -7,-3.134 -7,-7c0,-3.866 3.134,-7 7,-7c1.88,0 3.583,0.745 4.841,1.951l3.788,-3.27c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle cx="12" cy="2.5" r="1" style="fill:#b3b3b3;"/><circle cx="12" cy="21.5" r="1" style="fill:#b3b3b3;"/><circle cx="2.5" cy="12" r="1" style="fill:#b3b3b3;"/><path d="M4.575,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M18.01,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M4.575,4.575c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><circle id="XMLID_1329_" cx="12" cy="12" r="6" style="fill:#808080;"/><circle id="XMLID_1330_" cx="12" cy="12" r="7" style="fill:#808080;"/><path d="M19,12.25c0,-0.042 -0.006,-0.083 -0.006,-0.125c-0.068,3.808 -3.17,6.875 -6.994,6.875c-3.824,0 -6.933,-3.067 -7,-6.875c-0.001,0.042 0,0.083 0,0.125c0,3.866 3.134,7 7,7c3.866,0 7,-3.134 7,-7Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M18.92,13l-3.061,0c0.083,-0.321 0.141,-0.653 0.141,-1c0,-2.209 -1.791,-4 -4,-4c-2.209,0 -4,1.791 -4,4c0,1.105 0.448,2.105 1.172,2.828c1.014,1.015 4.057,4.058 4.057,4.058c2.955,-0.525 5.263,-2.899 5.691,-5.886Z" style="fill:#4d4d4d;fill-rule:nonzero;"/><path d="M22,13l-10,0c-0.553,0 -1,-0.448 -1,-1c0,-0.552 0.447,-1 1,-1l10,0c0.553,0 1,0.448 1,1c0,0.552 -0.447,1 -1,1Z" style="fill:#b3b3b3;fill-rule:nonzero;"/><path d="M11.948,11.25l10.104,0c0.409,0 0.776,0.247 0.935,0.592c-0.08,-0.471 -0.492,-0.842 -0.987,-0.842l-10,0c-0.495,0 -0.9,0.33 -0.98,0.801c0.159,-0.345 0.519,-0.551 0.928,-0.551Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M23,12c0,0.552 -0.447,1 -1,1l-3.08,0c-0.428,2.988 -2.737,5.362 -5.693,5.886l3.935,3.946c4.04,-1.931 6.838,-6.056 6.838,-10.832l-1,0Z" style="fill:#666;fill-opacity:0.5;fill-rule:nonzero;"/><path d="M12,5c-3.866,0 -7,3.134 -7,7c0,0.042 -0.001,0.069 0,0.111c0.067,-3.808 3.176,-6.861 7,-6.861c2.828,0 4.841,1.701 4.841,1.701c-1.257,-1.198 -2.968,-1.951 -4.841,-1.951Z" style="fill-opacity:0.1;fill-rule:nonzero;"/><circle id="XMLID_4_" cx="12" cy="12" r="12" style="fill:url(#_Linear1);"/></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(21.7566,10.1453,-10.1453,21.7566,1.12171,6.92737)"><stop offset="0" style="stop-color:#fff;stop-opacity:0.2"/><stop offset="1" style="stop-color:#fff;stop-opacity:0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Some files were not shown because too many files have changed in this diff Show More