Skip to content

Commit

Permalink
using passport as session manager
Browse files Browse the repository at this point in the history
  • Loading branch information
NebraskyTheWolf committed Jan 27, 2024
1 parent d8f0fb2 commit b64d623
Show file tree
Hide file tree
Showing 10 changed files with 592 additions and 394 deletions.
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"@types/mongoose": "^5.11.97",
"@types/node-cron": "^3.0.7",
"@types/ora": "^3.2.0",
"@types/passport": "^1.0.16",
"@types/passport-discord": "^0.1.11",
"@types/passport-local": "^1.0.38",
"@types/passport-oauth2-refresh": "^1.1.4",
"@types/redis": "^4.0.11",
"axios": "^1.6.6",
"body-parser": "^1.20.1",
Expand Down Expand Up @@ -67,6 +71,9 @@
"node-redis": "^0.1.7",
"ora": "^3.2.0",
"passport": "^0.6.0",
"passport-discord": "^0.1.4",
"passport-local": "^1.0.0",
"passport-oauth2-refresh": "^2.2.0",
"redis": "^4.6.12",
"rimraf": "^3.0.2",
"rollup": "^3.9.0",
Expand Down
11 changes: 4 additions & 7 deletions src/api/website/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Dashboard from './routes/Dashboard'
import { DiscordAccount } from '@riniya.ts/database/Social/DiscordAccount'
import cookieParser from 'cookie-parser'
import Shop from './routes/Shop'
import Passport from './passport'

const app = express();

Expand All @@ -27,19 +28,15 @@ export interface CustomResponse extends Response {
token?: DiscordAccount;
}

declare module 'express-session' {
interface SessionData {
account?: DiscordAccount
accountId?: string;
}
}

export default class WebsiteServer {
private routes: Tuple<AbstractRoutes>
private server: http.Server
private passport: Passport

public constructor() {
this.routes = new Tuple<AbstractRoutes>()
this.passport = new Passport()

app.set('trust proxy', 1) // trust first proxy

app.set('views', path.join(__dirname, 'views'));
Expand Down
48 changes: 5 additions & 43 deletions src/api/website/middleware/CAuthMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,15 @@
import BaseMiddleware from "../../Server/BaseMiddleware";
import { Response } from "express";
import { isNull } from "@riniya.ts/types";
import { CustomRequest, CustomResponse } from '../index'
import DiscordAccount from '@riniya.ts/database/Social/DiscordAccount'
import Discord from '../utils/Discord'
import Riniya from '@riniya.ts'
import { MessageEmbed } from 'discord.js'
import moment from 'moment/moment'

export default class CAuthMiddleware extends BaseMiddleware {

public constructor() {
super("ClientAuthentication", "Authentication middleware for the website")
}

public async handle(request: CustomRequest, response: CustomResponse, next) {
if (isNull(request.session.accountId)) {
return response.redirect("https://www.riniya.uk/user/login")
} else {
const account = await DiscordAccount.findOne({ _id: request.session.accountId })
if (isNull(account)) {
return response.redirect("https://www.riniya.uk")
}
const tokens = await Discord.getAccessToken(account.userId, account.tokens)
if (isNull(tokens.access_token)) {
// Deleting the account because the refresh token is invalidated.
await DiscordAccount.deleteOne({ _id: account._id })
}

await Riniya.instance.users.fetch(account.userId).then(user => {
user.send({
embeds: [
new MessageEmbed()
.setTitle("Dashboard Login detected.")
.setColor("RED")
.setDescription(`New login at ${moment(Date.now())}. If this action has been made on your behalf. Please terminate this session.`)
],
components: [
{
type: 1,
components: [
Riniya.instance.buttonManager.createLinkButton("Terminate", "https://api.riniya.uk/api/security/invalidate/" + account._id)
]
}
]
})
})

next()
}
if (request.isAuthenticated()) {
next()
} else {
return response.redirect("https://www.riniya.uk/user/login")
}
}
}
92 changes: 92 additions & 0 deletions src/api/website/passport/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Express, NextFunction } from 'express'
import DiscordPassport from "passport-discord";
import Refresh from "passport-oauth2-refresh";
import passport from "passport";
import DiscordAccount ,{ DiscordAccount as DAccount } from '@riniya.ts/database/Social/DiscordAccount'
import { isNull } from '@riniya.ts/types'
import Discord from '../utils/Discord'

const scopes = ['identify', 'email', 'guilds', 'guilds.join']

export interface User {
internal: string;
userId: string;
username: string;
email: string;
}

export interface IUser {
user: User;
avatar: string;
guilds: DiscordPassport.GuildInfo[]
}

export default class Passport {
public init(app: Express) {
app.use(passport.initialize())
app.use(passport.authenticate('session'))

const discordStrategy = new DiscordPassport.Strategy({
clientID: process.env["DISCORD_CLIENT_ID"],
clientSecret: process.env["DISCORD_CLIENT_SECRET"],
callbackURL: process.env["DISCORD_REDIRECT_URI"],
scope: scopes
},async function(accessToken, refreshToken, profile, cb) {
const account = await DiscordAccount?.exists({ userId: profile.id })
if (isNull(account)) {
new DiscordAccount({
userId: profile.id,
username: profile.username,
email: profile.email,
tokens: {
expires_at: 0,
expires_in: 0,
refresh_token: refreshToken,
access_token: accessToken
}
}).save().then(r => cb(null, {
user: {
internal: r._id,
userId: profile.id,
username: profile.username,
email: profile.email
},
avatar: profile.avatar,
guilds: profile.guilds
})).catch(err => cb(err))
} else {
Refresh.requestNewAccessToken('discord', refreshToken, async function(err, accessToken, refreshToken) {
if (err) {
throw err
}

await DiscordAccount.updateOne({ userId: profile.id }, {
$set: {
userId: profile.id,
username: profile.username,
email: profile.email,
tokens: {
expires_at: 0,
expires_in: 0,
refresh_token: refreshToken,
access_token: accessToken
}
}
}).then(r => cb(null, {
user: {
internal: r.upsertedId,
userId: profile.id,
username: profile.username,
email: profile.email
},
avatar: profile.avatar,
guilds: profile.guilds
}))
});
}
})

passport.use(discordStrategy)
Refresh.use(discordStrategy)
}
}
107 changes: 7 additions & 100 deletions src/api/website/routes/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,15 @@
import AbstractRoutes from '../../Server/AbstractRoutes'
import Discord from '../utils/Discord'
import { v4 } from 'uuid'
import { isNull } from '@riniya.ts/types'
import DiscordAccount from '@riniya.ts/database/Social/DiscordAccount'
import { CustomRequest, CustomResponse } from '../index'
import { JSONCookie } from 'cookie-parser'
import passport from 'passport'

export default class Auth extends AbstractRoutes {
async register () {
this.router.get('/user/login', async function (req: CustomRequest, res) {

let clientState = v4()

const oauthURL = await Discord.getOAuthUrl({
userUUID: clientState
})

res.cookie('clientState', clientState, {
maxAge: 1000 * 60 * 5,
signed: true
});
res.redirect(oauthURL.url.toString())
})

this.router.get('/user/callback', async function (req: CustomRequest, res: CustomResponse) {
req.session.destroy(err => {})
const code = String(req.query.code);
const discordState = String(req.query.state);
// make sure the state parameter exists
const clientState = req.signedCookies['clientState'];

if (clientState !== discordState) {
console.error('State verification failed.');
return res.status(403).json({
status: false,
error: "INVALID_CLIENT_STATE",
message: 'The client state value is unexpected.'
})
}
await Discord.getOAuthTokens(code).then(async result => {
if (isNull(result)) {
return res.status(403).json({
status: false,
error: "INVALID_STRUCT",
message: 'The request structure is invalid.'
})
}

await Discord.getUserData(result).then(async user => {
if (isNull(user)) {
return res.status(403).json({
status: false,
error: "INVALID_STRUCT",
message: 'Cannot fetch the user data.'
})
}
const userId = user.user.id;

const account = await DiscordAccount.findOne({ userId: userId }, { tokens: -1, __v: -1})

if (isNull(account._id)) {
await new DiscordAccount({
userId: userId,
email: user.user.email,
username: user.user.username,
tokens: {
access_token: result.access_token,
refresh_token: result.refresh_token,
expires_in: result.expires_in,
expires_at: Date.now() + result.expires_in * 1000
},
uuid: discordState
}).save().then(r => {
req.session.account = r
req.session.accountId = r._id
req.session.save()
})
} else {
await DiscordAccount.updateOne({
userId: userId
}, {
$set: {
userId: userId,
email: user.user.email,
username: user.user.username,
tokens: {
access_token: result.access_token,
refresh_token: result.refresh_token,
expires_in: result.expires_in,
expires_at: Date.now() + result.expires_in * 1000
},
uuid: discordState
}
})

req.session.account = account
req.session.accountId = account._id
req.session.save()
}

res.status(200).redirect("https://www.riniya.uk/dashboard")
})
this.router.get('/user/login', passport.authenticate("discord"));
this.router.get('/user/callback', passport.authenticate('discord', {
failureRedirect: '/',
}), function(req, res) {
res.render('dashboard', {
user: req.user
})
})
}

}
7 changes: 4 additions & 3 deletions src/api/website/routes/Dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import AbstractRoutes from '../../Server/AbstractRoutes'
import { CustomRequest } from '../index'
import { Request } from "express"
import { IUser } from '../passport'

export default class Dashboard extends AbstractRoutes {
async register () {
this.isProtected = true;

this.router.get('/', function (req: CustomRequest, res) {
this.router.get('/', function (req: Request, res) {
res.render('dashboard', {
user: req.session.account
user: req.user as IUser
})
})
}
Expand Down
7 changes: 4 additions & 3 deletions src/api/website/routes/Shop.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import AbstractRoutes from '../../Server/AbstractRoutes'
import { CustomRequest } from '../index'
import { Request } from "express"
import { IUser } from '../passport'

export default class Shop extends AbstractRoutes {
async register () {
this.router.get('/', function (req: CustomRequest, res) {
this.router.get('/', function (req: Request, res) {
res.render('shop', {
user: req.token
user: (req.user as IUser)
})
})
}
Expand Down
4 changes: 2 additions & 2 deletions src/api/website/views/dashboard.pug
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
section.ui.container.fluid
a.ui.red.label=user.username
a.ui.yellow.label=user.email
a.ui.red.label=user.user.username
a.ui.yellow.label=user.user.email
a.ui.red.label Welcome!
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ import InitChecker from '@riniya.ts/utils/InitChecker';
import { createClient, RedisClientType } from "redis";

import CacheController from "./database/CacheController";
import { DiscordAccount } from './database/Models/Social/DiscordAccount'

declare module 'express-session' {
interface SessionData {
account?: DiscordAccount
accountId?: string;
}
}

export default class Riniya extends Client {
public static instance: Riniya
Expand Down Expand Up @@ -168,4 +176,4 @@ export default class Riniya extends Client {
}
}

export const riniya: Riniya = new Riniya()
export const riniya: Riniya = new Riniya()
Loading

0 comments on commit b64d623

Please sign in to comment.