Compare commits
4 Commits
2e4af394f9
...
860a29f626
Author | SHA1 | Date |
---|---|---|
Jyotirmoy Bandyopadhayaya | 860a29f626 | |
Jyotirmoy Bandyopadhayaya | 34e2a94fce | |
Jyotirmoy Bandyopadhayaya | 2544ce8031 | |
Jyotirmoy Bandyopadhayaya | edb2544e60 |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
}
|
||||
}
|
|
@ -129,3 +129,9 @@ dist
|
|||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
**/jwtRS256.key
|
||||
**/jwtRS256.key.pub
|
||||
|
||||
**/yarn.lock
|
||||
.husky
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
19
package.json
19
package.json
|
@ -7,16 +7,27 @@
|
|||
"license": "MIT",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||
"@typescript-eslint/parser": "^5.54.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"husky": "^8.0.2",
|
||||
"lerna": "^6.1.0",
|
||||
"lint-staged": "^13.0.4",
|
||||
"turbo": "^1.6.3"
|
||||
"lint-staged": "^13.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky install"
|
||||
"configure-husky": "npx husky install && npx husky add .husky/pre-commit \"npx --no-install lint-staged\""
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
"**/*.{js,json,ts,css}": [
|
||||
"eslint --fix",
|
||||
"prettier --write \"**/*.{ts,tsx,js,jsx,json,css,scss,md}\""
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
|
@ -29,4 +29,9 @@ MAL_CLIENT_SECRET=
|
|||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_REGION=
|
||||
WAKATIME_API_KEY=
|
||||
WAKATIME_API_KEY=
|
||||
SESSION_SECRET=
|
||||
KEYCLOAK_CLIENT_ID=
|
||||
KEYCLOAK_CLIENT_SECRET=
|
||||
KEYCLOAK_REDIRECT_URI=
|
||||
KEYCLOAK_AUTH_SERVER_URL=
|
|
@ -0,0 +1,22 @@
|
|||
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 }
|
|
@ -0,0 +1,37 @@
|
|||
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
|
|
@ -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 }
|
||||
)
|
||||
}
|
|
@ -1,9 +1,15 @@
|
|||
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,12 +38,19 @@ 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 {
|
||||
public configStoreType: IconfigStore
|
||||
|
||||
constructor(isProd: boolean = false) {
|
||||
constructor(isProd = false) {
|
||||
if (isProd) {
|
||||
this.configStoreType = 'production'
|
||||
} else {
|
||||
|
@ -46,9 +59,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<IConfigKeys> = await parseFile(envContent)
|
||||
env.PUBLIC_KEY = publicKEY
|
||||
env.PRIVATE_KEY = privateKEY
|
||||
env.KEYCLOAK_ISSUER = keyCloakIssuer
|
||||
return env
|
||||
} else {
|
||||
let reqEnvContent: any = await fs.readFileSync(
|
||||
|
@ -57,8 +75,11 @@ export default class ConfigStoreFactory {
|
|||
)
|
||||
reqEnvContent = reqEnvContent.replaceAll('=', '')
|
||||
reqEnvContent = reqEnvContent.split('\n')
|
||||
let missingKeys: string[] = []
|
||||
let env: Partial<IConfigKeys> = {}
|
||||
const missingKeys: string[] = []
|
||||
const env: Partial<IConfigKeys> = {}
|
||||
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 +88,7 @@ export default class ConfigStoreFactory {
|
|||
if (missingKeys.length > 0) {
|
||||
throw new Error(`Missing keys: ${missingKeys}`)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
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))
|
||||
}
|
||||
}
|
|
@ -4,11 +4,10 @@ import { Request, Response } from 'express'
|
|||
|
||||
export default class MastodonController extends MastodonService {
|
||||
public fetchMastodonProfile = async (req: Request, res: Response) => {
|
||||
try{
|
||||
try {
|
||||
const data = await this.getMastodonProfile()
|
||||
return res.send(makeResponse(data))
|
||||
}
|
||||
catch (err: any){
|
||||
} catch (err: any) {
|
||||
res.send(makeResponse(err.message, {}, 'Failed', true))
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +16,7 @@ export default class MastodonController extends MastodonService {
|
|||
try {
|
||||
const data = await this.getMastodonStatuses()
|
||||
return res.send(makeResponse(data))
|
||||
}
|
||||
catch (err: any) {
|
||||
} catch (err: any) {
|
||||
res.send(makeResponse(err.message, {}, 'Failed', true))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
actions: []
|
||||
custom_types:
|
||||
enums: []
|
||||
input_objects: []
|
||||
objects: []
|
||||
scalars: []
|
||||
enums: []
|
||||
input_objects: []
|
||||
objects: []
|
||||
scalars: []
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
disabled_for_roles: []
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1,21 @@
|
|||
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 }
|
|
@ -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) {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -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
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
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
|
|
@ -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
|
||||
|
|
|
@ -12,7 +12,7 @@ export default class MastodonService {
|
|||
const { data } = await axiosInstance.get(
|
||||
'https://fosstodon.org/api/v1/accounts/109612266657666903/statuses',
|
||||
{
|
||||
timeout: 10000
|
||||
timeout: 10000,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body class="container">
|
||||
|
||||
<header>
|
||||
<%- include('../partials/header'); %>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="jumbotron">
|
||||
<a href="/auth/signin">
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exampleModal">
|
||||
Sign In
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body class="container">
|
||||
|
||||
<header>
|
||||
<%- include('../partials/header'); %>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="jumbotron">
|
||||
<h1>API Landing</h1>
|
||||
<p>Welcome to B68 API Home</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
<p class="text-center text-muted">© Copyright 2023 <a href="https://itsmebravo.dev">Jyotirmoy Bandyopadhayaya</a> | <a href="https://github.com/bravo68web">Github</a></p>
|
|
@ -0,0 +1,15 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>B68 API</title>
|
||||
|
||||
<!-- CSS (load bootstrap from a CDN) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<style>
|
||||
body { padding-top:50px; }
|
||||
.jumbotron {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">B68 API</a>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/auth">Auth</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
Loading…
Reference in New Issue