mirror of
https://github.com/roleypoly/roleypoly-v1.git
synced 2025-04-25 12:19:10 +00:00
add AuthService as a DM authentication handler. (NEEDS SECURITY PASS)
This commit is contained in:
parent
02a66ee7b0
commit
f30ca78e40
6 changed files with 154 additions and 4 deletions
|
@ -8,6 +8,7 @@ import logger from './logger'
|
||||||
import ServerService from './services/server'
|
import ServerService from './services/server'
|
||||||
import DiscordService from './services/discord'
|
import DiscordService from './services/discord'
|
||||||
import SessionService from './services/sessions'
|
import SessionService from './services/sessions'
|
||||||
|
import AuthService from './services/auth'
|
||||||
import PresentationService from './services/presentation'
|
import PresentationService from './services/presentation'
|
||||||
import RPCServer from './rpc'
|
import RPCServer from './rpc'
|
||||||
import fetchModels, { type Models } from './models'
|
import fetchModels, { type Models } from './models'
|
||||||
|
@ -43,7 +44,8 @@ export type AppContext = {
|
||||||
P: PresentationService,
|
P: PresentationService,
|
||||||
RPC: RPCServer,
|
RPC: RPCServer,
|
||||||
M: Models,
|
M: Models,
|
||||||
sql: Sequelize
|
sql: Sequelize,
|
||||||
|
auth: AuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
class Roleypoly {
|
class Roleypoly {
|
||||||
|
@ -112,6 +114,7 @@ class Roleypoly {
|
||||||
this.ctx.server = new ServerService(this.ctx)
|
this.ctx.server = new ServerService(this.ctx)
|
||||||
this.ctx.discord = new DiscordService(this.ctx)
|
this.ctx.discord = new DiscordService(this.ctx)
|
||||||
this.ctx.sessions = new SessionService(this.ctx)
|
this.ctx.sessions = new SessionService(this.ctx)
|
||||||
|
this.ctx.auth = new AuthService(this.ctx)
|
||||||
this.ctx.P = new PresentationService(this.ctx)
|
this.ctx.P = new PresentationService(this.ctx)
|
||||||
this.ctx.RPC = new RPCServer(this)
|
this.ctx.RPC = new RPCServer(this)
|
||||||
}
|
}
|
||||||
|
|
24
api/auth.js
24
api/auth.js
|
@ -2,6 +2,8 @@
|
||||||
import { type Context } from 'koa'
|
import { type Context } from 'koa'
|
||||||
import { type AppContext, type Router } from '../Roleypoly'
|
import { type AppContext, type Router } from '../Roleypoly'
|
||||||
import ksuid from 'ksuid'
|
import ksuid from 'ksuid'
|
||||||
|
import logger from '../logger'
|
||||||
|
const log = logger(__filename)
|
||||||
|
|
||||||
export default (R: Router, $: AppContext) => {
|
export default (R: Router, $: AppContext) => {
|
||||||
R.post('/api/auth/token', async (ctx: Context) => {
|
R.post('/api/auth/token', async (ctx: Context) => {
|
||||||
|
@ -35,7 +37,7 @@ export default (R: Router, $: AppContext) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
R.get('/api/auth/user', async (ctx: Context) => {
|
R.get('/api/auth/user', async (ctx: Context) => {
|
||||||
const { accessToken } = (ctx.session: { accessToken?: string })
|
const { accessToken } = ((ctx.session: any): { accessToken?: string })
|
||||||
if (accessToken === undefined) {
|
if (accessToken === undefined) {
|
||||||
ctx.body = { err: 'not_logged_in' }
|
ctx.body = { err: 'not_logged_in' }
|
||||||
ctx.status = 401
|
ctx.status = 401
|
||||||
|
@ -81,4 +83,24 @@ export default (R: Router, $: AppContext) => {
|
||||||
R.get('/api/oauth/bot/callback', async (ctx: Context) => {
|
R.get('/api/oauth/bot/callback', async (ctx: Context) => {
|
||||||
// console.log(ctx.request)
|
// console.log(ctx.request)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
R.get('/magic/:challenge', async (ctx: Context) => {
|
||||||
|
const { challenge } = ((ctx.params: any): { challenge: string })
|
||||||
|
const chall = await $.auth.fetchDMChallenge({ magic: challenge })
|
||||||
|
if (chall == null) {
|
||||||
|
log.warn('bad magic', challenge)
|
||||||
|
ctx.status = 404
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.session = {
|
||||||
|
userId: chall.userId,
|
||||||
|
authType: 'dm',
|
||||||
|
expiresAt: Date.now() + 1000 * 60 * 60 * 24
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('logged in via magic', chall)
|
||||||
|
|
||||||
|
return ctx.redirect('/')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,12 @@ import chalk from 'chalk'
|
||||||
export class Logger {
|
export class Logger {
|
||||||
debugOn: boolean
|
debugOn: boolean
|
||||||
name: string
|
name: string
|
||||||
|
quietSql: boolean
|
||||||
|
|
||||||
constructor (name: string, debugOverride: boolean = false) {
|
constructor (name: string, debugOverride: boolean = false) {
|
||||||
this.name = name
|
this.name = name
|
||||||
this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride
|
this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride
|
||||||
|
this.quietSql = (process.env.DEBUG_SQL !== 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
fatal (text: string, ...data: any) {
|
fatal (text: string, ...data: any) {
|
||||||
|
@ -52,7 +54,7 @@ export class Logger {
|
||||||
}
|
}
|
||||||
|
|
||||||
sql (logger: Logger, ...data: any) {
|
sql (logger: Logger, ...data: any) {
|
||||||
if (logger.debugOn) {
|
if (logger.debugOn && !logger.quietSql) {
|
||||||
console.log(chalk.bold('DEBUG SQL:\n '), data)
|
console.log(chalk.bold('DEBUG SQL:\n '), data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
models/AuthChallenge.js
Normal file
14
models/AuthChallenge.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// @flow
|
||||||
|
import type Sequelize, { DataTypes as DT } from 'sequelize'
|
||||||
|
|
||||||
|
export default (sql: Sequelize, DataTypes: DT) => {
|
||||||
|
return sql.define('auth_challenge', {
|
||||||
|
userId: DataTypes.TEXT,
|
||||||
|
issuedAt: DataTypes.DATE,
|
||||||
|
type: DataTypes.ENUM('dm', 'other'),
|
||||||
|
human: { type: DataTypes.TEXT, unique: true },
|
||||||
|
magic: { type: DataTypes.TEXT, unique: true }
|
||||||
|
}, {
|
||||||
|
indexes: [ { fields: ['userId'] } ]
|
||||||
|
})
|
||||||
|
}
|
51
services/auth.js
Normal file
51
services/auth.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// @flow
|
||||||
|
import Service from './Service'
|
||||||
|
import nanoid from 'nanoid'
|
||||||
|
import moniker from 'moniker'
|
||||||
|
import type { AppContext } from '../Roleypoly'
|
||||||
|
// import type { Models } from '../models'
|
||||||
|
|
||||||
|
export type DMChallenge = {
|
||||||
|
userId: string, // snowflake of the user
|
||||||
|
human: string, // humanized string for input elsewhere, adjective adjective noun
|
||||||
|
magic: string, // magic URL, a nanoid
|
||||||
|
issuedAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AuthService extends Service {
|
||||||
|
M: { AuthChallenge: any }
|
||||||
|
monikerGen = moniker.generator([ moniker.adjective, moniker.adjective, moniker.noun ], { glue: ' ' })
|
||||||
|
constructor (ctx: AppContext) {
|
||||||
|
super(ctx)
|
||||||
|
this.M = ctx.M
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDMChallenge (userId: string): Promise<DMChallenge> {
|
||||||
|
const out: DMChallenge = {
|
||||||
|
userId,
|
||||||
|
human: this.monikerGen.choose(),
|
||||||
|
magic: nanoid(10),
|
||||||
|
issuedAt: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.M.AuthChallenge.build({ ...out, type: 'dm' }).save()
|
||||||
|
this.log.debug('created DM auth challenge', out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchDMChallenge (input: { human: string } | { magic: string }): Promise<?DMChallenge> {
|
||||||
|
const challenge: ?DMChallenge = this.M.AuthChallenge.findOne({ where: input })
|
||||||
|
if (challenge == null) {
|
||||||
|
this.log.debug('challenge not found', challenge)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// if issued more than 1 hour ago, it doesn't matter.
|
||||||
|
if (+challenge.issuedAt + 3.6e5 < new Date()) {
|
||||||
|
this.log.debug('challenge expired', challenge)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return challenge
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import {
|
||||||
type Role,
|
type Role,
|
||||||
type GuildMember,
|
type GuildMember,
|
||||||
type Collection,
|
type Collection,
|
||||||
|
type User,
|
||||||
Client
|
Client
|
||||||
} from 'discord.js'
|
} from 'discord.js'
|
||||||
|
|
||||||
|
@ -172,6 +173,20 @@ class DiscordService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserPartial (userId: string): ?UserPartial {
|
||||||
|
const U = this.client.users.get(userId)
|
||||||
|
if (U == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: U.username,
|
||||||
|
discriminator: U.discriminator,
|
||||||
|
avatar: U.displayAvatarURL,
|
||||||
|
id: U.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// on sign out, we revoke the token we had.
|
// on sign out, we revoke the token we had.
|
||||||
// async revokeAuthToken (code, state) {
|
// async revokeAuthToken (code, state) {
|
||||||
// const url = 'https://discordapp.com/api/oauth2/revoke'
|
// const url = 'https://discordapp.com/api/oauth2/revoke'
|
||||||
|
@ -269,11 +284,54 @@ class DiscordService extends Service {
|
||||||
this.mentionResponse(message)
|
this.mentionResponse(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async issueChallenge (message: Message) {
|
||||||
|
// Create a challenge
|
||||||
|
const chall = await this.ctx.auth.createDMChallenge(message.author.id)
|
||||||
|
|
||||||
|
const randomLines = [
|
||||||
|
'🐄 A yellow cow is only as bright as it lets itself be. ✨',
|
||||||
|
'‼ **Did you know?** On this day, at least one second ago, you were right here!',
|
||||||
|
'<:AkkoC8:437428070849314816> *Reticulating splines...*',
|
||||||
|
'Also, you look great today <:YumekoWink:439519270376964107>',
|
||||||
|
'btw, ur bright like a <:diamond:544665968631087125>',
|
||||||
|
`🌈 psst! pssssst! I'm an expensive bot, would you please spare some change? <https://patreon.com/kata>`,
|
||||||
|
'📣 have suggestions? wanna help out? join my discord! <https://discord.gg/PWQUVsd>\n*(we\'re nice people, i swear!)*',
|
||||||
|
`🤖 this bot is at least ${Math.random() * 100}% LIT 🔥`,
|
||||||
|
'💖 wanna contribute to these witty lines? <https://discord.gg/PWQUVsd> suggest them on our discord!',
|
||||||
|
'🛠 I am completely open source, check me out!~ <https://github.com/kayteh/roleypoly>'
|
||||||
|
]
|
||||||
|
|
||||||
|
message.channel.send([
|
||||||
|
'**Hey there!** <a:StockKyaa:435353158462603266>',
|
||||||
|
'',
|
||||||
|
`Use this secret code: **${chall.human}**`,
|
||||||
|
`Or, click here: ${this.ctx.config.appUrl}/magic/${chall.magic}`,
|
||||||
|
'',
|
||||||
|
'This code will self-destruct in 1 hour.',
|
||||||
|
'---',
|
||||||
|
randomLines[Math.floor(Math.random() * randomLines.length)]
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDM (message: Message) {
|
||||||
|
switch (message.content.toLowerCase()) {
|
||||||
|
case 'login':
|
||||||
|
case 'auth':
|
||||||
|
case 'log in':
|
||||||
|
this.issueChallenge(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleMessage (message: Message) {
|
handleMessage (message: Message) {
|
||||||
if (message.author.bot || message.channel.type !== 'text') { // drop bot messages and dms
|
if (message.author.bot) { // drop bot messages
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.channel.type === 'dm') {
|
||||||
|
// handle dm
|
||||||
|
return this.handleDM(message)
|
||||||
|
}
|
||||||
|
|
||||||
if (message.mentions.users.has(this.client.user.id)) {
|
if (message.mentions.users.has(this.client.user.id)) {
|
||||||
if (this.rootUsers.has(message.author.id)) {
|
if (this.rootUsers.has(message.author.id)) {
|
||||||
this.handleCommand(message)
|
this.handleCommand(message)
|
||||||
|
|
Loading…
Add table
Reference in a new issue