Compare commits

...

4 Commits

35 changed files with 341 additions and 8527 deletions

21
.eslintrc Normal file
View File

@ -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
}
}

6
.gitignore vendored
View File

@ -129,3 +129,9 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
**/jwtRS256.key
**/jwtRS256.key.pub
**/yarn.lock
.husky

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@ -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/*"

View File

@ -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=

View File

@ -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 }

View File

@ -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

View File

@ -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 }
)
}

View File

@ -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
}
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -1,6 +1,6 @@
actions: []
custom_types:
enums: []
input_objects: []
objects: []
scalars: []
enums: []
input_objects: []
objects: []
scalars: []

View File

@ -0,0 +1 @@
{}

View File

@ -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"

View File

@ -0,0 +1 @@
disabled_for_roles: []

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
[]

View File

@ -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 }

View File

@ -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) {

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1 @@
<p class="text-center text-muted">&copy; Copyright 2023 <a href="https://itsmebravo.dev">Jyotirmoy Bandyopadhayaya</a> | <a href="https://github.com/bravo68web">Github</a></p>

View File

@ -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>

View File

@ -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>

8475
yarn.lock

File diff suppressed because it is too large Load Diff