1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-03 21:10:05 +00:00

feat: enabled session based authentication for web

This commit is contained in:
Saad Jutt
2022-04-28 06:44:25 +05:00
parent a30fb1a241
commit 5da93f318a
25 changed files with 582 additions and 300 deletions

View File

@@ -1,5 +1,7 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import morgan from 'morgan'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
@@ -34,6 +36,25 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
app.use(cors({ credentials: true, origin: whiteList }))
}
if (MODE?.trim() === 'server') {
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
const { PROTOCOL } = process.env
app.use(
session({
secret: process.env.SESSION_SECRET as string,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store: MongoStore.create({ clientPromise, collectionName: 'sessions' }),
cookie: {
secure: PROTOCOL === 'https',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
})
)
}
app.use(cookieParser())
app.use(morgan('tiny'))
app.use(express.json({ limit: '100mb' }))
@@ -61,6 +82,5 @@ export default setProcessVariables().then(async () => {
app.use(onError)
await connectDB()
return app
})

View File

@@ -24,7 +24,7 @@ export class SessionController {
}
const session = (req: any) => ({
id: req.user.id,
id: req.user.userId,
username: req.user.username,
displayName: req.user.displayName
})

View File

@@ -0,0 +1,75 @@
import express from 'express'
import { Request, Route, Tags, Post, Body, Get } from 'tsoa'
import User from '../model/User'
@Route('/')
@Tags('Web')
export class WebController {
/**
* @summary Accept a valid username/password
*
*/
@Post('/login')
public async login(
@Request() req: express.Request,
@Body() body: LoginPayload
) {
return login(req, body)
}
/**
* @summary Accept a valid username/password
*
*/
@Get('/logout')
public async logout(@Request() req: express.Request) {
return new Promise((resolve) => {
req.session.destroy(() => {
resolve(true)
})
})
}
}
const login = async (
req: express.Request,
{ username, password }: LoginPayload
) => {
// Authenticate User
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
req.session.loggedIn = true
req.session.user = {
userId: user.id,
clientId: 'web_app',
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive
}
return {
loggedIn: true,
user: {
username: user.username,
displayName: user.displayName
}
}
}
interface LoginPayload {
/**
* Username for user
* @example "secretuser"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
}

View File

@@ -2,6 +2,10 @@ import jwt from 'jsonwebtoken'
import { verifyTokenInDB } from '../utils'
export const authenticateAccessToken = (req: any, res: any, next: any) => {
if (req.session?.loggedIn) {
req.user = req.session.user
return next()
}
authenticateToken(
req,
res,
@@ -43,9 +47,7 @@ const authenticateToken = (
}
const authHeader = req.headers['authorization']
const token =
authHeader?.split(' ')[1] ??
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
const token = authHeader?.split(' ')[1]
if (!token) return res.sendStatus(401)
jwt.verify(token, key, async (err: any, data: any) => {

View File

@@ -32,9 +32,8 @@ authRouter.post('/token', async (req, res) => {
try {
const response = await controller.token(body)
const { accessToken } = response
res.cookie('accessToken', accessToken).send(response)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}

View File

@@ -1,37 +1,40 @@
import { readFile } from '@sasjs/utils'
import express from 'express'
import path from 'path'
import { getWebBuildFolderPath } from '../../utils'
import express from 'express'
import { fileExists } from '@sasjs/utils'
import { WebController } from '../../controllers/web'
import { getWebBuildFolderPath, loginWebValidation } from '../../utils'
const webRouter = express.Router()
const jsCodeForDesktopMode = `
<script>
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
</script>`
const jsCodeForServerMode = `
<script>
localStorage.setItem('CLIENT_ID', '${process.env.CLIENT_ID}')
</script>`
webRouter.get('/', async (_, res) => {
let content: string
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
if (await fileExists(indexHtmlPath)) return res.sendFile(indexHtmlPath)
return res.send('Web Build is not present')
})
webRouter.post('/login', async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const controller = new WebController()
try {
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
content = await readFile(indexHtmlPath)
} catch (_) {
return res.send('Web Build is not present')
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
res.status(400).send(err.toString())
}
})
const { MODE } = process.env
const codeToInject =
MODE?.trim() === 'server' ? jsCodeForServerMode : jsCodeForDesktopMode
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
res.setHeader('Content-Type', 'text/html')
return res.send(injectedContent)
webRouter.get('/logout', async (req, res) => {
const controller = new WebController()
try {
await controller.logout(req)
res.status(200).send()
} catch (err: any) {
res.status(400).send(err.toString())
}
})
export default webRouter

View File

@@ -1,8 +0,0 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
driveLoc: string
sessionController?: import('../controllers/internal').SessionController
appStreamConfig: import('./').AppStreamConfig
}
}

View File

@@ -1,17 +0,0 @@
import { MacroVars } from '@sasjs/utils'
export interface ExecutionQuery {
_program: string
macroVars?: MacroVars
_debug?: number
}
export interface FileQuery {
filePath: string
}
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
arg && !Array.isArray(arg) && typeof arg._program === 'string'
export const isFileQuery = (arg: any): arg is FileQuery =>
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'

View File

@@ -3,6 +3,5 @@ export * from './AppStreamConfig'
export * from './Execution'
export * from './InfoJWT'
export * from './PreProgramVars'
export * from './Request'
export * from './Session'
export * from './TreeNode'

View File

@@ -0,0 +1,14 @@
import express from 'express'
declare module 'express-session' {
interface SessionData {
loggedIn: boolean
user: {
userId: number
clientId: string
username: string
displayName: string
isAdmin: boolean
isActive: boolean
}
}
}

1
api/src/types/system/global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import 'jest-extended'

8
api/src/types/system/process.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
driveLoc: string
sessionController?: import('../../controllers/internal').SessionController
appStreamConfig: import('../').AppStreamConfig
}
}

View File

@@ -11,15 +11,18 @@ export const connectDB = async () => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
console.log('Running in Desktop Mode, no DB to connect.')
return
}
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err
try {
await mongoose.connect(process.env.DB_CONNECT as string)
} catch (err) {
throw new Error('Unable to connect to DB!')
}
console.log('Connected to db!')
console.log('Connected to DB!')
await seedDB()
await seedDB()
})
return mongoose.connection
}

View File

@@ -5,6 +5,12 @@ const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
export const loginWebValidation = (data: any): Joi.ValidationResult =>
Joi.object({
username: usernameSchema.required(),
password: passwordSchema.required()
}).validate(data)
export const authorizeValidation = (data: any): Joi.ValidationResult =>
Joi.object({
username: usernameSchema.required(),