mirror of
https://github.com/roleypoly/roleypoly-v1.git
synced 2025-04-25 04:09:12 +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 DiscordService from './services/discord'
|
||||
import SessionService from './services/sessions'
|
||||
import AuthService from './services/auth'
|
||||
import PresentationService from './services/presentation'
|
||||
import RPCServer from './rpc'
|
||||
import fetchModels, { type Models } from './models'
|
||||
|
@ -43,7 +44,8 @@ export type AppContext = {
|
|||
P: PresentationService,
|
||||
RPC: RPCServer,
|
||||
M: Models,
|
||||
sql: Sequelize
|
||||
sql: Sequelize,
|
||||
auth: AuthService
|
||||
}
|
||||
|
||||
class Roleypoly {
|
||||
|
@ -112,6 +114,7 @@ class Roleypoly {
|
|||
this.ctx.server = new ServerService(this.ctx)
|
||||
this.ctx.discord = new DiscordService(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.RPC = new RPCServer(this)
|
||||
}
|
||||
|
|
24
api/auth.js
24
api/auth.js
|
@ -2,6 +2,8 @@
|
|||
import { type Context } from 'koa'
|
||||
import { type AppContext, type Router } from '../Roleypoly'
|
||||
import ksuid from 'ksuid'
|
||||
import logger from '../logger'
|
||||
const log = logger(__filename)
|
||||
|
||||
export default (R: Router, $: AppContext) => {
|
||||
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) => {
|
||||
const { accessToken } = (ctx.session: { accessToken?: string })
|
||||
const { accessToken } = ((ctx.session: any): { accessToken?: string })
|
||||
if (accessToken === undefined) {
|
||||
ctx.body = { err: 'not_logged_in' }
|
||||
ctx.status = 401
|
||||
|
@ -81,4 +83,24 @@ export default (R: Router, $: AppContext) => {
|
|||
R.get('/api/oauth/bot/callback', async (ctx: Context) => {
|
||||
// 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 {
|
||||
debugOn: boolean
|
||||
name: string
|
||||
quietSql: boolean
|
||||
|
||||
constructor (name: string, debugOverride: boolean = false) {
|
||||
this.name = name
|
||||
this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride
|
||||
this.quietSql = (process.env.DEBUG_SQL !== 'true')
|
||||
}
|
||||
|
||||
fatal (text: string, ...data: any) {
|
||||
|
@ -52,7 +54,7 @@ export class Logger {
|
|||
}
|
||||
|
||||
sql (logger: Logger, ...data: any) {
|
||||
if (logger.debugOn) {
|
||||
if (logger.debugOn && !logger.quietSql) {
|
||||
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 GuildMember,
|
||||
type Collection,
|
||||
type User,
|
||||
Client
|
||||
} 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.
|
||||
// async revokeAuthToken (code, state) {
|
||||
// const url = 'https://discordapp.com/api/oauth2/revoke'
|
||||
|
@ -269,11 +284,54 @@ class DiscordService extends Service {
|
|||
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) {
|
||||
if (message.author.bot || message.channel.type !== 'text') { // drop bot messages and dms
|
||||
if (message.author.bot) { // drop bot messages
|
||||
return
|
||||
}
|
||||
|
||||
if (message.channel.type === 'dm') {
|
||||
// handle dm
|
||||
return this.handleDM(message)
|
||||
}
|
||||
|
||||
if (message.mentions.users.has(this.client.user.id)) {
|
||||
if (this.rootUsers.has(message.author.id)) {
|
||||
this.handleCommand(message)
|
||||
|
|
Loading…
Add table
Reference in a new issue