diff --git a/.gitignore b/.gitignore index c9281ab..8f82434 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +**/jwtRS256.key +**/jwtRS256.key.pub diff --git a/packages/api/auth/check.ts b/packages/api/auth/check.ts new file mode 100644 index 0000000..2ef2609 --- /dev/null +++ b/packages/api/auth/check.ts @@ -0,0 +1,25 @@ +import { authClient } from "../helpers/auth_client"; +import { generators } from 'openid-client'; +import { configKeys } from ".."; + +const code_verifier = generators.codeVerifier(); +const code_challenge = generators.codeChallenge(code_verifier); + +export default () => { + const authurl = authClient.authorizationUrl({ + scope: 'email profile openid', + code_challenge, + code_challenge_method: 'S256', + client_id: configKeys.KEYCLOAK_CLIENT_ID, + redirect_uri: configKeys.KEYCLOAK_REDIRECT_URI, + }); + + return { + authurl, + }; +} + +export { + code_challenge, + code_verifier, +} \ No newline at end of file diff --git a/packages/api/auth/middleware.ts b/packages/api/auth/middleware.ts new file mode 100644 index 0000000..720e37e --- /dev/null +++ b/packages/api/auth/middleware.ts @@ -0,0 +1,32 @@ +import { NextFunction, Response } from "express"; +import {authClient} from "../helpers/auth_client"; +import { ModRequest } from "../types"; +import { CustomError } from "../libs/error"; + +const middleware = async (req: ModRequest | any, res: Response, next: NextFunction) => { + try { + const authHeader = req.headers?.authorization?.split(' '); + if (!authHeader) { + throw new Error("No authorization header"); + } + const token : string = authHeader[1]; + const user = await authClient.userinfo(token, { + method: 'GET', + tokenType: 'Bearer', + params: { + access_token: token + }, + via: 'header' + }); + req.user = user; + next(); + } + catch (err) { + next(new CustomError({ + message: "Invalid token", + statusCode: 401 + })) + } +} + +export default middleware \ No newline at end of file diff --git a/packages/api/auth/verify.ts b/packages/api/auth/verify.ts new file mode 100644 index 0000000..8b55cb5 --- /dev/null +++ b/packages/api/auth/verify.ts @@ -0,0 +1,10 @@ +import {authClient} from "../helpers/auth_client"; +import { code_verifier } from './check' + +export default async (session_state: string, code: string) => { + return authClient.callback( + 'http://localhost:4038/auth/signin/callback', + { code_verifier, code, session_state, expires_in: "1d" }, + { code_verifier } + ); +} \ No newline at end of file diff --git a/packages/api/configs/index.ts b/packages/api/configs/index.ts index efe6943..f9f729f 100644 --- a/packages/api/configs/index.ts +++ b/packages/api/configs/index.ts @@ -1,9 +1,13 @@ import fs from 'fs' import { parse as parseFile } from 'envfile' +import { Issuer } from 'openid-client'; + +const keyCloakIssuer : Issuer = await Issuer.discover(process.env.KEYCLOAK_AUTH_SERVER_URL!); +console.log('šŸ” Connected to Keycloak'); type IconfigStore = 'development' | 'production' -interface IConfigKeys { +export interface IConfigKeys { PORT: string | number NODE_ENV: string HASURA_GRAPHQL_ADMIN_SECRET: string @@ -32,6 +36,13 @@ interface IConfigKeys { AWS_ACCESS_KEY_ID: string AWS_SECRET_ACCESS_KEY: string AWS_REGION: string + PUBLIC_KEY: string + PRIVATE_KEY: string + KEYCLOAK_ISSUER: Issuer + KEYCLOAK_CLIENT_ID: string + KEYCLOAK_CLIENT_SECRET: string + KEYCLOAK_REDIRECT_URI: string + KEYCLOAK_AUTH_SERVER_URL: string } export default class ConfigStoreFactory { @@ -46,9 +57,14 @@ export default class ConfigStoreFactory { } public async getConfigStore() { + const publicKEY = fs.readFileSync('./jwtRS256.key', 'utf8'); + const privateKEY = fs.readFileSync('./jwtRS256.key.pub', 'utf8'); if (this.configStoreType === 'development') { const envContent = await fs.readFileSync(`./.env`, 'utf8') const env: Partial = await parseFile(envContent) + env.PUBLIC_KEY = publicKEY + env.PRIVATE_KEY = privateKEY + env.KEYCLOAK_ISSUER = keyCloakIssuer return env } else { let reqEnvContent: any = await fs.readFileSync( @@ -59,6 +75,9 @@ export default class ConfigStoreFactory { reqEnvContent = reqEnvContent.split('\n') let missingKeys: string[] = [] let env: Partial = {} + env.PUBLIC_KEY = publicKEY + env.PRIVATE_KEY = privateKEY + env.KEYCLOAK_ISSUER = keyCloakIssuer for (const line of reqEnvContent) { if (!process.env[line]) { missingKeys.push(line) @@ -67,6 +86,7 @@ export default class ConfigStoreFactory { if (missingKeys.length > 0) { throw new Error(`Missing keys: ${missingKeys}`) } + return env } } diff --git a/packages/api/controllers/auth.controller.ts b/packages/api/controllers/auth.controller.ts new file mode 100644 index 0000000..ab58744 --- /dev/null +++ b/packages/api/controllers/auth.controller.ts @@ -0,0 +1,21 @@ +import { Request, Response } from 'express' +import { makeResponse } from '../libs' +import check from '../auth/check' +import verify from '../auth/verify' +import { ModRequest } from '../types' + +export default class AuthController { + public signin = (req: Request, res: Response) => { + const { authurl } = check() + res.redirect(authurl) + } + + public callback = async (req: Request, res: Response) => { + const { session_state, code } = req.query as { session_state: string, code: string} + res.send(makeResponse(await verify(session_state, code))) + } + + public me = (req: ModRequest, res: Response) => { + res.send(makeResponse(req.user)) + } +} \ No newline at end of file diff --git a/packages/api/hasura/metadata/actions.yaml b/packages/api/hasura/metadata/actions.yaml index 1979211..1edb4c2 100644 --- a/packages/api/hasura/metadata/actions.yaml +++ b/packages/api/hasura/metadata/actions.yaml @@ -1,6 +1,6 @@ actions: [] custom_types: - enums: [] - input_objects: [] - objects: [] - scalars: [] + enums: [] + input_objects: [] + objects: [] + scalars: [] diff --git a/packages/api/hasura/metadata/api_limits.yaml b/packages/api/hasura/metadata/api_limits.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/packages/api/hasura/metadata/api_limits.yaml @@ -0,0 +1 @@ +{} diff --git a/packages/api/hasura/metadata/databases/databases.yaml b/packages/api/hasura/metadata/databases/databases.yaml index fe51488..b25fa9e 100644 --- a/packages/api/hasura/metadata/databases/databases.yaml +++ b/packages/api/hasura/metadata/databases/databases.yaml @@ -1 +1,9 @@ -[] +- name: default + kind: postgres + configuration: + connection_info: + database_url: + from_env: PG_DATABASE_URL + isolation_level: read-committed + use_prepared_statements: false + tables: "!include default/tables/tables.yaml" diff --git a/packages/api/hasura/metadata/databases/default/tables/tables.yaml b/packages/api/hasura/metadata/databases/default/tables/tables.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/packages/api/hasura/metadata/databases/default/tables/tables.yaml @@ -0,0 +1 @@ +[] diff --git a/packages/api/hasura/metadata/graphql_schema_introspection.yaml b/packages/api/hasura/metadata/graphql_schema_introspection.yaml new file mode 100644 index 0000000..61a4dca --- /dev/null +++ b/packages/api/hasura/metadata/graphql_schema_introspection.yaml @@ -0,0 +1 @@ +disabled_for_roles: [] diff --git a/packages/api/hasura/metadata/inherited_roles.yaml b/packages/api/hasura/metadata/inherited_roles.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/packages/api/hasura/metadata/inherited_roles.yaml @@ -0,0 +1 @@ +[] diff --git a/packages/api/hasura/metadata/network.yaml b/packages/api/hasura/metadata/network.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/packages/api/hasura/metadata/network.yaml @@ -0,0 +1 @@ +{} diff --git a/packages/api/hasura/metadata/rest_endpoints.yaml b/packages/api/hasura/metadata/rest_endpoints.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/packages/api/hasura/metadata/rest_endpoints.yaml @@ -0,0 +1 @@ +[] diff --git a/packages/api/helpers/auth_client.ts b/packages/api/helpers/auth_client.ts new file mode 100644 index 0000000..d7298f2 --- /dev/null +++ b/packages/api/helpers/auth_client.ts @@ -0,0 +1,19 @@ +import { configKeys } from ".."; + +const { KEYCLOAK_ISSUER } = configKeys + +const authConfig = { + client_id: configKeys.KEYCLOAK_CLIENT_ID, + 'auth-server-url': configKeys.KEYCLOAK_AUTH_SERVER_URL, + 'ssl-required': 'all', + resource: configKeys.KEYCLOAK_CLIENT_ID, + credentials: { 'secret-jwt': { secret: configKeys.KEYCLOAK_CLIENT_SECRET } }, + 'confidential-port': 0, + redirect_uri: configKeys.KEYCLOAK_REDIRECT_URI, + client_secret: configKeys.KEYCLOAK_CLIENT_SECRET, + default_max_age: 3600000, +} + +const authClient = new KEYCLOAK_ISSUER.Client(authConfig) + +export { authClient } \ No newline at end of file diff --git a/packages/api/helpers/cache.factory.ts b/packages/api/helpers/cache.factory.ts index 37ad812..2eb71a0 100644 --- a/packages/api/helpers/cache.factory.ts +++ b/packages/api/helpers/cache.factory.ts @@ -43,7 +43,7 @@ export default class CacheClient { } this._nodeClient = new NodeCache() - console.log(`Caching Client initialized in '${env}' environment`) + console.log(`šŸž Caching Client initialized in '${env}' environment`) } static async set(key: string, value: any) { diff --git a/packages/api/index.ts b/packages/api/index.ts index 4b8378f..5b46eb6 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -5,45 +5,40 @@ import morgan from 'morgan' import helmet from 'helmet' import { hgqlInit } from './helpers' +import cacheClient from './helpers/cache.factory'; import routes from './routes' import { errorHandler, notFoundHandler } from './libs' import pkg from './package.json' assert { type: 'json' } -import configStore from './configs' +import configStore, { IConfigKeys } from './configs'; export const app: express.Application = express() -hgqlInit() - console.log('šŸš€', '@b68/api', 'v' + pkg.version) +hgqlInit() +cacheClient.init(); + const isDev: boolean = process.env.NODE_ENV == 'production' -console.log(isDev ? 'šŸš€ Production Mode' : 'šŸš€ Development Mode') +console.log(isDev ? 'šŸš€ Production Mode' : 'šŸ‘· Development Mode') const configs = new configStore(isDev) -const configKeys: any = await configs.getConfigStore() +const configKeys: IConfigKeys = await configs.getConfigStore() as IConfigKeys app.use(cors()) app.use(helmet()) app.use(morgan('dev')) app.use(express.json()) app.use(express.urlencoded({ extended: true, limit: '50mb' })) +app.set('view engine', 'ejs'); -app.use('/health', (req, res) => { - return res.status(200).json({ - app: pkg.name, - request_ip: req.ip, - uptime: process.uptime(), - hrtime: process.hrtime(), - }) -}) +console.log('ā˜„ ', 'Base Route', '/') -console.log('ā˜„', 'Base Route', '/') app.use('/', routes) app.use(notFoundHandler) app.use(errorHandler) -app.listen(process.env.PORT, async () => { - console.log(`\nServer running on port ${process.env.PORT}`) +app.listen(configKeys.PORT, async () => { + console.log(`\nšŸŒˆ Server running on port ${configKeys.PORT}`) }) export { configKeys } diff --git a/packages/api/package.json b/packages/api/package.json index dd2a9a7..ce7c6ae 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -14,6 +14,7 @@ "axios": "^1.2.1", "cors": "^2.8.5", "dotenv": "^16.0.3", + "ejs": "^3.1.9", "envfile": "^6.18.0", "express": "^4.18.2", "form-data": "^4.0.0", @@ -27,6 +28,7 @@ "napi-nanoid": "^0.0.4", "node-cache": "^5.1.2", "nodemailer": "^6.8.0", + "openid-client": "^5.4.0", "osu-api-extended": "^2.5.12", "redis": "^4.5.1", "typescript": "^4.9.3" diff --git a/packages/api/public/favicon.ico b/packages/api/public/favicon.ico new file mode 100644 index 0000000..3f464f3 Binary files /dev/null and b/packages/api/public/favicon.ico differ diff --git a/packages/api/routes/auth.routes.ts b/packages/api/routes/auth.routes.ts index d82a717..f90476d 100644 --- a/packages/api/routes/auth.routes.ts +++ b/packages/api/routes/auth.routes.ts @@ -1,18 +1,18 @@ import { Router } from 'express' -import { makeResponse } from '../libs' +import AuthController from '../controllers/auth.controller' +import middleware from '../auth/middleware' const router = Router() +const authController = new AuthController() -router.get('/', (req, res) => { - res.send(makeResponse({ message: 'Hello World auth!' })) -}) +router.get('/signin', authController.signin) -router.all('/err', async (req, res, next) => { - try { - throw new Error('This is an error') - } catch (err) { - next(err) - } -}) +router.get('/signin/callback', authController.callback) + +router.get('/me', middleware, authController.me as any) + +router.get('/', function(req, res) { + res.render('pages/auth'); +}); export default router diff --git a/packages/api/routes/health.routes.ts b/packages/api/routes/health.routes.ts new file mode 100644 index 0000000..a4147a0 --- /dev/null +++ b/packages/api/routes/health.routes.ts @@ -0,0 +1,16 @@ +import { Router } from 'express' + +const router = Router() + +router.use('/health', + (req, res) => { + return res.status(200).json({ + status: "OK", + app: "B68 API", + request_ip: req.ip, + uptime: process.uptime(), + hrtime: process.hrtime(), + }) +}) + +export default router diff --git a/packages/api/routes/index.ts b/packages/api/routes/index.ts index be7fecd..1adeb73 100644 --- a/packages/api/routes/index.ts +++ b/packages/api/routes/index.ts @@ -30,7 +30,7 @@ const loadRoutes = async (dirPath: string, prefix = '/') => { '' ) const modRoute = path.join(prefix, route) - console.log('šŸ›°ļø', 'Loaded', modRoute) + console.log('šŸ›°ļø ', 'Loaded', modRoute) const mod = await import(path.join(baseDir, prefix + f.name)) router.use(modRoute, mod.default) @@ -48,4 +48,13 @@ let baseDir = path.dirname(__filename) baseDir = path.resolve(baseDir) loadRoutes(baseDir) + +router.get('/', function(req, res) { + res.render('pages/index'); +}); + +router.get('/favicon.ico', function(req, res) { + res.sendFile(path.join(__dirname, '../public', 'favicon.ico')); +}); + export default router diff --git a/packages/api/types/index.d.ts b/packages/api/types/index.d.ts index 73af2e7..b47ae84 100644 --- a/packages/api/types/index.d.ts +++ b/packages/api/types/index.d.ts @@ -1,3 +1,5 @@ +import { Request } from 'express' + export interface PaginationType { page: number limit: number @@ -5,3 +7,7 @@ export interface PaginationType { sort_by?: string filters?: { [k: string]: any } } + +export interface ModRequest extends Request { + user: any +} \ No newline at end of file diff --git a/packages/api/views/pages/auth.ejs b/packages/api/views/pages/auth.ejs new file mode 100644 index 0000000..672ed67 --- /dev/null +++ b/packages/api/views/pages/auth.ejs @@ -0,0 +1,27 @@ + + + + <%- include('../partials/head'); %> + + + +
+ <%- include('../partials/header'); %> +
+ +
+ +
+ +
+ <%- include('../partials/footer'); %> +
+ + + \ No newline at end of file diff --git a/packages/api/views/pages/index.ejs b/packages/api/views/pages/index.ejs new file mode 100644 index 0000000..3a7d7c4 --- /dev/null +++ b/packages/api/views/pages/index.ejs @@ -0,0 +1,24 @@ + + + + <%- include('../partials/head'); %> + + + +
+ <%- include('../partials/header'); %> +
+ +
+
+

API Landing

+

Welcome to B68 API Home

+
+
+ +
+ <%- include('../partials/footer'); %> +
+ + + \ No newline at end of file diff --git a/packages/api/views/partials/footer.ejs b/packages/api/views/partials/footer.ejs new file mode 100644 index 0000000..55de3d0 --- /dev/null +++ b/packages/api/views/partials/footer.ejs @@ -0,0 +1 @@ +

© Copyright 2023 Jyotirmoy Bandyopadhayaya | Github

\ No newline at end of file diff --git a/packages/api/views/partials/head.ejs b/packages/api/views/partials/head.ejs new file mode 100644 index 0000000..6929440 --- /dev/null +++ b/packages/api/views/partials/head.ejs @@ -0,0 +1,15 @@ + +B68 API + + + + + \ No newline at end of file diff --git a/packages/api/views/partials/header.ejs b/packages/api/views/partials/header.ejs new file mode 100644 index 0000000..cff0e8d --- /dev/null +++ b/packages/api/views/partials/header.ejs @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3993607..c357360 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3754,6 +3754,13 @@ ejs@^3.1.7: dependencies: jake "^10.8.5" +ejs@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" + integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + dependencies: + jake "^10.8.5" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -5332,6 +5339,11 @@ joi@^17.7.0: "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" +jose@^4.10.0: + version "4.14.0" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.0.tgz#c8c03579a0ba3598194c92ccca777d96adca8f48" + integrity sha512-LSA/XenLPwqk6e2L+PSUNuuY9G4NGsvjRWz6sJcUBmzTLEPJqQh46FHSUxnAQ64AWOkRO6bSXpy3yXuEKZkbIA== + js-sdsl@^4.1.4: version "4.2.0" resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz" @@ -6464,6 +6476,11 @@ object-assign@^4, object-assign@^4.1.1: resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-hash@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.12.2, object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" @@ -6519,6 +6536,11 @@ object.values@^1.1.5, object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +oidc-token-hash@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.2.tgz#f9ca7f7f1f92d721a2973e66b7430cb52a486648" + integrity sha512-U91Ba78GtVBxcExLI7U+hC2AwJQqXQEW/D3fjmJC4hhSVIgdl954KO4Gu95WqAlgDKJdLATxkmuxraWLT0fVRQ== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" @@ -6568,6 +6590,16 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openid-client@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.4.0.tgz#77f1cda14e2911446f16ea3f455fc7c405103eac" + integrity sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ== + dependencies: + jose "^4.10.0" + lru-cache "^6.0.0" + object-hash "^2.0.1" + oidc-token-hash "^5.0.1" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz"