Adding Basic API Documentation

This commit is contained in:
Fabian Stamm
2019-03-14 18:10:39 -04:00
parent e09c6df9f6
commit 18ea5de8aa
21 changed files with 828 additions and 97 deletions

View File

@ -1,29 +1,60 @@
import { Router, Request } from "express";
import { GetUserMiddleware } from "../middlewares/user";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
import Client from "../../models/client";
import User from "../../models/user";
import verify, { Types } from "../middlewares/verify";
import { randomBytes } from "crypto";
const ClientRouter: Router = Router();
ClientRouter.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
ClientRouter.route("/")
/**
* @api {get} /admin/client
* @apiName AdminGetClients
*
* @apiGroup admin_client
* @apiPermission admin
*
* @apiSuccess {Object[]} clients
* @apiSuccess {String} clients._id The internally used id
* @apiSuccess {String} clients.maintainer
* @apiSuccess {Boolean} clients.internal
* @apiSuccess {String} clients.name
* @apiSuccess {String} clients.redirect_url
* @apiSuccess {String} clients.website
* @apiSuccess {String} clients.logo
* @apiSuccess {String} clients.client_id Client ID used outside of DB
* @apiSuccess {String} clients.client_secret
*/
.get(promiseMiddleware(async (req, res) => {
let clients = await Client.find({});
//ToDo check if user is required!
res.json(clients);
}))
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
await Client.delete(id);
res.json({ success: true });
}))
/**
* @api {get} /admin/client
* @apiName AdminAddClients
*
* @apiGroup admin_client
* @apiPermission admin
*
* @apiParam {Boolean} internal Is it an internal app
* @apiParam {String} name
* @apiParam {String} redirect_url
* @apiParam {String} website
* @apiParam {String} logo
*
* @apiSuccess {Object[]} clients
* @apiSuccess {String} clients._id The internally used id
* @apiSuccess {String} clients.maintainer
* @apiSuccess {Boolean} clients.internal
* @apiSuccess {String} clients.name
* @apiSuccess {String} clients.redirect_url
* @apiSuccess {String} clients.website
* @apiSuccess {String} clients.logo
* @apiSuccess {String} clients.client_id Client ID used outside of DB
* @apiSuccess {String} clients.client_secret
*/
.post(verify({
internal: {
type: Types.BOOLEAN,
@ -49,11 +80,48 @@ ClientRouter.route("/")
await Client.save(client)
res.json(client);
}))
ClientRouter.route("/:id")
/**
* @api {delete} /admin/client/:id
* @apiParam {String} id Client _id
* @apiName AdminDeleteClient
*
* @apiGroup admin_client
* @apiPermission admin
*
* @apiSuccess {Boolean} success
*/
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.params;
await Client.delete(id);
res.json({ success: true });
}))
/**
* @api {put} /admin/client/:id
* @apiParam {String} id Client _id
* @apiName AdminUpdateClient
*
* @apiGroup admin_client
* @apiPermission admin
*
* @apiParam {Boolean} internal Is it an internal app
* @apiParam {String} name
* @apiParam {String} redirect_url
* @apiParam {String} website
* @apiParam {String} logo
*
* @apiSuccess {String} _id The internally used id
* @apiSuccess {String} maintainer UserID of client maintainer
* @apiSuccess {Boolean} internal Defines if it is a internal client
* @apiSuccess {String} name The name of the Client
* @apiSuccess {String} redirect_url Redirect URL after login
* @apiSuccess {String} website Website of Client
* @apiSuccess {String} logo The Logo of the Client (optional)
* @apiSuccess {String} client_id Client ID used outside of DB
* @apiSuccess {String} client_secret The client secret, that can be used to obtain token
*/
.put(verify({
id: {
type: Types.STRING,
query: true
},
internal: {
type: Types.BOOLEAN,
optional: true
@ -85,4 +153,5 @@ ClientRouter.route("/")
res.json(client);
}))
export default ClientRouter;

View File

@ -3,8 +3,16 @@ import ClientRoute from "./client";
import UserRoute from "./user";
import RegCodeRoute from "./regcode";
import PermissionRoute from "./permission";
import { GetUserMiddleware } from "../middlewares/user";
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()
});
AdminRoute.use("/client", ClientRoute);
AdminRoute.use("/regcode", RegCodeRoute)
AdminRoute.use("/user", UserRoute)

View File

@ -8,13 +8,22 @@ import Client from "../../models/client";
import { ObjectID } from "bson";
const PermissionRoute: Router = Router();
PermissionRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
PermissionRoute.route("/")
/**
* @api {get} /admin/permission
* @apiName AdminGetPermissions
*
* @apiParam client Optionally filter by client _id
*
* @apiGroup admin_permission
* @apiPermission admin
*
* @apiSuccess {Object[]} permissions
* @apiSuccess {String} permissions._id The ID
* @apiSuccess {String} permissions.name Permission name
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
* @apiSuccess {String} permissions.client The ID of the owning client
*/
.get(promiseMiddleware(async (req, res) => {
let query = {};
if (req.query.client) {
@ -23,6 +32,23 @@ PermissionRoute.route("/")
let permission = await Permission.find(query);
res.json(permission);
}))
/**
* @api {post} /admin/permission
* @apiName AdminAddPermission
*
* @apiParam client The ID of the owning client
* @apiParam name Permission name
* @apiParam description A description, that makes it clear to the user, what this Permission allows to do
*
* @apiGroup admin_permission
* @apiPermission admin
*
* @apiSuccess {Object[]} permissions
* @apiSuccess {String} permissions._id The ID
* @apiSuccess {String} permissions.name Permission name
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
* @apiSuccess {String} permissions.client The ID of the owning client
*/
.post(verify({
client: {
type: Types.STRING
@ -45,7 +71,19 @@ PermissionRoute.route("/")
});
await Permission.save(permission);
res.json(permission);
})).delete(promiseMiddleware(async (req, res) => {
}))
/**
* @api {delete} /admin/permission
* @apiName AdminDeletePermission
*
* @apiParam id The permission ID
*
* @apiGroup admin_permission
* @apiPermission admin
*
* @apiSuccess {Boolean} success
*/
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
await Permission.delete(id);
res.json({ success: true });

View File

@ -7,20 +7,49 @@ import { GetUserMiddleware } from "../middlewares/user";
import { HttpStatusCode } from "../../helper/request_error";
const RegCodeRoute: Router = Router();
RegCodeRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
RegCodeRoute.route("/")
/**
* @api {get} /admin/regcode
* @apiName AdminGetRegcodes
*
* @apiGroup admin_regcode
* @apiPermission admin
*
* @apiSuccess {Object[]} regcodes
* @apiSuccess {String} permissions._id The ID
* @apiSuccess {String} permissions.token The Regcode Token
* @apiSuccess {String} permissions.valid Defines if the Regcode is valid
* @apiSuccess {String} permissions.validTill Expiration date of RegCode
*/
.get(promiseMiddleware(async (req, res) => {
let regcodes = await RegCode.find({});
res.json(regcodes);
}))
/**
* @api {delete} /admin/regcode
* @apiName AdminDeleteRegcode
*
* @apiParam {String} id The id of the RegCode
*
* @apiGroup admin_regcode
* @apiPermission admin
*
* @apiSuccess {Boolean} success
*/
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
await RegCode.delete(id);
res.json({ success: true });
}))
/**
* @api {post} /admin/regcode
* @apiName AdminAddRegcode
*
* @apiGroup admin_regcode
* @apiPermission admin
*
* @apiSuccess {String} code The newly created code
*/
.post(promiseMiddleware(async (req, res) => {
let regcode = RegCode.new({
token: randomBytes(10).toString("hex"),

View File

@ -14,10 +14,37 @@ UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
})
UserRoute.route("/")
/**
* @api {get} /admin/user
* @apiName AdminGetUsers
*
* @apiGroup admin_user
* @apiPermission admin
* @apiSuccess {Object[]} user
* @apiSuccess {String} user._id The internal id of the user
* @apiSuccess {String} user.uid The public UID of the user
* @apiSuccess {String} user.username The username
* @apiSuccess {String} user.name The real name
* @apiSuccess {Date} user.birthday The birthday
* @apiSuccess {Number} user.gender 0 = none, 1 = male, 2 = female, 3 = other
* @apiSuccess {Boolean} user.admin Is admin or not
*/
.get(promiseMiddleware(async (req, res) => {
let users = await User.find({});
users.forEach(e => delete e.password && delete e.salt && delete e.encryption_key);
res.json(users);
}))
/**
* @api {delete} /admin/user
* @apiName AdminDeleteUser
*
* @apiParam {String} id The User ID
*
* @apiGroup admin_user
* @apiPermission admin
*
* @apiSuccess {Boolean} success
*/
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
let user = await User.findById(id);
@ -32,7 +59,23 @@ UserRoute.route("/")
await User.delete(user);
res.json({ success: true });
})).put(promiseMiddleware(async (req, res) => {
}))
/**
* @api {put} /admin/user
* @apiName AdminChangeUser
*
* @apiParam {String} id The User ID
*
* @apiGroup admin_user
* @apiPermission admin
*
* @apiSuccess {Boolean} success
*
* @apiDescription Flipps the user role:
* admin -> user
* user -> admin
*/
.put(promiseMiddleware(async (req, res) => {
let { id } = req.query;
let user = await User.findById(id);
user.admin = !user.admin;

View File

@ -0,0 +1,27 @@
import { Request, Response, Router } from "express"
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys";
const ClientRouter = Router();
/**
* @api {get} /client/user
* @apiName ClientUser
*
* @apiGroup client
* @apiPermission user_client Requires ClientID and Authenticated User
*
* @apiParam {String} redirect_uri URL to redirect to on success
*/
ClientRouter.get("/user", Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(true, false), async (req: Request, res: Response) => {
let jwt = await createJWT({
client: req.client.client_id,
uid: req.user.uid,
username: req.user.username
}, 30); //after 30 seconds this token is invalid
res.redirect(req.query.redirect_uri + "?jwt=" + jwt)
}));
export default ClientRouter;

View File

@ -1,14 +0,0 @@
import { Request, Response } from "express"
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys";
export const AuthGetUser = Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(true, false), async (req: Request, res: Response) => {
let jwt = await createJWT({
client: req.client.client_id,
uid: req.user.uid,
username: req.user.username
}, 30); //after 30 seconds this token is invalid
res.redirect(req.query.redirect_uri + "?jwt=" + jwt)
});

View File

@ -3,7 +3,7 @@ import AdminRoute from "./admin";
import UserRoute from "./user";
import InternalRoute from "./internal";
import Login from "./user/login";
import { AuthGetUser } from "./client/user";
import ClientRouter from "./client";
import * as cors from "cors";
import OAuthRoute from "./oauth";
@ -14,10 +14,10 @@ ApiRouter.use("/user", UserRoute);
ApiRouter.use("/internal", InternalRoute);
ApiRouter.use("/oauth", OAuthRoute);
ApiRouter.use("/client/user", AuthGetUser);
ApiRouter.use("/client", ClientRouter);
// Legacy reasons (deprecated)
ApiRouter.use("/user", AuthGetUser);
ApiRouter.use("/", ClientRouter);
// Legacy reasons (deprecated)
ApiRouter.post("/login", Login);

View File

@ -3,6 +3,28 @@ import { OAuthInternalApp } from "./oauth";
import PasswordAuth from "./password";
const InternalRoute: Router = Router();
/**
* @api {get} /internal/oauth
* @apiName ClientInteralOAuth
*
* @apiGroup client_internal
* @apiPermission client_internal Only ClientID
*
* @apiParam {String} redirect_uri Redirect URI called after success
* @apiParam {String} state State will be set in RedirectURI for the client to check
*/
InternalRoute.get("/oauth", OAuthInternalApp);
/**
* @api {post} /internal/password
* @apiName ClientInteralPassword
*
* @apiGroup client_internal
* @apiPermission client_internal Requires ClientID and Secret
*
* @apiParam {String} username Username (either username or UID)
* @apiParam {String} uid User ID (either username or UID)
* @apiParam {String} password Hashed and Salted according to specification
*/
InternalRoute.post("/password", PasswordAuth)
export default InternalRoute;

View File

@ -5,9 +5,59 @@ import Public from "./public";
import RefreshTokenRoute from "./refresh";
const OAuthRoue: Router = Router();
/**
* @api {post} /oauth/auth
* @apiName OAuthAuth
*
* @apiGroup oauth
* @apiPermission user Special required
*
* @apiParam {String} response_type must be "code" others are not supported
* @apiParam {String} client_id ClientID
* @apiParam {String} redirect_uri The URI to redirect with code
* @apiParam {String} scope Scope that contains the requested permissions (comma seperated list of permissions)
* @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", AuthRoute);
/**
* @api {get} /oauth/jwt
* @apiName OAuthJwt
*
* @apiGroup oauth
* @apiPermission none
*
* @apiParam {String} refreshtoken
*
* @apiSuccess {String} token The JWT that allowes the application to access the recources granted for refresh token
*/
OAuthRoue.get("/jwt", JWTRoute)
/**
* @api {get} /oauth/public
* @apiName OAuthPublic
*
* @apiGroup oauth
* @apiPermission none
*
* @apiSuccess {String} public_key The applications public_key. Used to verify JWT.
*/
OAuthRoue.get("/public", Public)
/**
* @api {get} /oauth/refresh
* @apiName OAuthRefreshGet
*
* @apiGroup oauth
*/
OAuthRoue.get("/refresh", RefreshTokenRoute);
/**
* @api {post} /oauth/refresh
* @apiName OAuthRefreshPost
*
* @apiGroup oauth
*/
OAuthRoue.post("/refresh", RefreshTokenRoute);
export default OAuthRoue;

View File

@ -3,7 +3,6 @@ import promiseMiddleware from "../../helper/promiseMiddleware";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import RefreshToken from "../../models/refresh_token";
import User from "../../models/user";
import Permission from "../../models/permissions";
import Client from "../../models/client";
import getOAuthJWT from "../../helper/jwt";
@ -16,7 +15,9 @@ const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
let user = await User.findById(token.user);
if (!user) {
//TODO handle error!
token.valid = false;
await RefreshToken.save(token);
throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
}
let client = await Client.findById(token.client);

View File

@ -5,10 +5,91 @@ import TwoFactorRoute from "./twofactor";
import { GetToken, DeleteToken } from "./token";
const UserRoute: Router = Router();
/**
* @api {post} /user/register
* @apiName UserRegister
*
* @apiGroup user
* @apiPermission none
*
* @apiParam {String} mail EMail linked to this Account
* @apiParam {String} username The new Username
* @apiParam {String} password Password hashed and salted like specification
* @apiParam {String} salt The Salt used for password hashing
* @apiParam {String} regcode The regcode, that should be used
* @apiParam {String} gender Gender can be: "male", "female", "other", "none"
* @apiParam {String} name The real name of the User
*
* @apiSuccess {Boolean} success
*
* @apiErrorExample {Object} Error-Response:
{
error: [
{
message: "Some Error",
field: "username"
}
],
status: 400
}
*/
UserRoute.post("/register", Register);
/**
* @api {post} /user/login?type=:type
* @apiName UserLogin
*
* @apiParam {String} type Type could be either "username" or "password"
*
* @apiGroup user
* @apiPermission none
*
* @apiParam {String} username Username (either username or uid required)
* @apiParam {String} uid (either username or uid required)
* @apiParam {String} password Password hashed and salted like specification (only on type password)
*
* @apiSuccess {String} uid On type = "username"
* @apiSuccess {String} salt On type = "username"
*
* @apiSuccess {String} login On type = "password". Login Token
* @apiSuccess {String} special On type = "password". Special Token
* @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required
* @apiSuccess {String} tfa.id The ID of the TFA Method
* @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.use("/twofactor", TwoFactorRoute);
/**
* @api {get} /user/token
* @apiName UserGetToken
*
* @apiGroup user
* @apiPermission user
*
* @apiSuccess {Object[]} token
* @apiSuccess {String} token.id The Token ID
* @apiSuccess {String} token.special Identifies Special Token
* @apiSuccess {String} token.ip IP the token was optained from
* @apiSuccess {String} token.browser The Browser the token was optained from (User Agent)
* @apiSuccess {Boolean} token.isthis Shows if it is token used by this session
*/
UserRoute.get("/token", GetToken);
UserRoute.delete("/token", DeleteToken);
/**
* @api {delete} /user/token/:id
* @apiParam {String} id The id of the token to be deleted
*
* @apiName UserDeleteToken
*
* @apiParam {String} type Type could be either "username" or "password"
*
* @apiGroup user
* @apiPermission user
*
* @apiSuccess {Boolean} success
*/
UserRoute.delete("/token/:id", DeleteToken);
export default UserRoute;

View File

@ -3,9 +3,7 @@ import User, { IUser } from "../../models/user";
import { randomBytes } from "crypto";
import moment = require("moment");
import LoginToken from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
import * as speakeasy from "speakeasy";
import TwoFactor from "../../models/twofactor";
const Login = promiseMiddleware(async (req: Request, res: Response) => {
@ -19,49 +17,49 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
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 client = {
ip: Array.isArray(ip) ? ip[0] : ip,
browser: req.headers["user-agent"]
}
const sendToken = async (user: IUser, tfa?: any[]) => {
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress
let client = {
ip: Array.isArray(ip) ? ip[0] : ip,
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 token = LoginToken.new({
token: token_str,
valid: true,
validTill: tfa ? tfa_exp : token_exp,
user: user._id,
validated: tfa ? false : true,
...client
});
await LoginToken.save(token);
let special_str = randomBytes(24).toString("hex");
let special_exp = moment().add(30, "minutes").toDate()
let special = LoginToken.new({
token: special_str,
valid: true,
validTill: tfa ? tfa_exp : special_exp,
special: true,
user: user._id,
validated: tfa ? false : true,
...client
});
await LoginToken.save(special);
res.json({
login: { token: token_str, expires: token.validTill.toUTCString() },
special: { token: special_str, expires: special.validTill.toUTCString() },
tfa
});
}
let token_str = randomBytes(16).toString("hex");
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
});
await LoginToken.save(token);
let special_str = randomBytes(24).toString("hex");
let special_exp = moment().add(30, "minutes").toDate()
let special = LoginToken.new({
token: special_str,
valid: true,
validTill: tfa ? tfa_exp : special_exp,
special: true,
user: user._id,
validated: tfa ? false : true,
...client
});
await LoginToken.save(special);
res.json({
login: { token: token_str, expires: token.validTill.toUTCString() },
special: { token: special_str, expires: special.validTill.toUTCString() },
tfa
});
}
if (type === "password") {
let { username, password, uid } = req.body;
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })

View File

@ -6,7 +6,6 @@ import TwoFactor from "../../../models/twofactor";
import * as moment from "moment"
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
const TwoFactorRouter = Router();
TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {

View File

@ -82,9 +82,10 @@ export default class Web {
private registerErrorHandler() {
this.server.use((error, req: express.Request, res, next) => {
if (!(error instanceof RequestError)) {
Logging.error(error);
error = new RequestError(error.message, HttpStatusCode.INTERNAL_SERVER_ERROR);
} else if (error.status === 500 && !(<any>error).nolog) {
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(typeof error.message === "string" ? error.message.split("\n", 1)[0] : error.message);