Skip to content

Commit

Permalink
feat: context is entity with services, replace migrations
Browse files Browse the repository at this point in the history
remove a bunch of service locator/direct wiring of service dependencies
in favor of context

BREAKING CHANGE: database must be reset and migrations run again
  • Loading branch information
ssube committed Dec 9, 2018
1 parent 7d77446 commit dc0b6b1
Show file tree
Hide file tree
Showing 46 changed files with 787 additions and 569 deletions.
5 changes: 2 additions & 3 deletions docs/listener/discord-listener.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ data:
game:
name: Global Thermonuclear Warfare
sessionProvider:
metadata:
kind: auth-controller
name: default-auth
kind: auth-controller
name: default-auth
token: !env ISOLEX_DISCORD_TOKEN
7 changes: 6 additions & 1 deletion docs/listener/express-listener.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ data:
metrics: true
listen:
port: 4000
address: "0.0.0.0"
address: "0.0.0.0"
token:
audience: test
issuer: test
scheme: isolex
secret: test-secret-foo
6 changes: 4 additions & 2 deletions docs/listener/slack-listener.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ metadata:
kind: slack-listener
name: slack-isolex
data:
token: !env ISOLEX_SLACK_TOKEN
presence:
game:
name: Global Thermonuclear Warfare

sessionProvider:
kind: auth-controller
name: default-auth
token: !env ISOLEX_SLACK_TOKEN
30 changes: 30 additions & 0 deletions docs/sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Sessions

The authentication classes in isolex supports JWT-based RBAC and sessions.

While authentication for chat and HTTP is fundamentally the same, the protocols have radically different ways
of tracking users. Most chat applications, for example, have already authenticated a user and established a session
of their own. HTTP has no such session, until provided by a cookie.

The authentication controller calls the underlying whatever to create a session. This includes the listener from the
context. That means context should not be saved in the database, which makes sense.

The session whatever notifies the listener that a session has been established between a user (based on the context
passed) and the user fetched.

TOKENS HAVE NOTHING TO DO WITH SESSIONS
TOKENS PROVIDE THE INITIAL LOOKUP TO ASSOCIATE A USER WITH A LISTENER
THAT IS A SESSION

What is context?

Context is:

- source listener (service)
- target ? (always starts equal to source, can change when message becomes command or for completion)
- optional user (entity, loaded)
- session data (flash)

## Tokens

Tokens are the only
12 changes: 12 additions & 0 deletions docs/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ This document covers Typescript and YAML style, explains some lint rules, and ma
- [Paths](#paths)
- [Typescript](#typescript)
- [Destructuring](#destructuring)
- [Entities](#entities)
- [Exports](#exports)
- [Imports](#imports)
- [Order](#order)
- [Properties](#properties)
- [Tests](#tests)
- [Async](#async)
- [Assertions](#assertions)
Expand Down Expand Up @@ -61,6 +63,8 @@ Messages are sent.

## Typescript

Dictionary objects (`{...}`) must always be treated as immutable.

### Destructuring

Destructuring is great, use it! Groups should be `{ spaced, out }` like imports (lint will warn about this, code can
Expand All @@ -70,6 +74,10 @@ Never nest destructuring. Defaults are ok.

Prefer destructuring with default over `||`. For example, `const { foo = 3 } = bar;` over `const foo = bar.foo || 3;`.

### Entities

Always provide the table name as an exported constant and use it in `@Entity(TABLE_FOO)` and the migrations.

### Exports

Never use default exports.
Expand All @@ -92,6 +100,10 @@ Always `import { by, name }`, unless using a broken old library that required `i
Ensure imports are sorted alphabetically, even within a single line. Your editor should be able to do this for you,
because it is extremely tedious to do by hand.

### Properties

Object properties should not be nullable or optional unless absolutely needed. Prefer sensible defaults.

### Tests

Typescript tests (small, unit tests) are run using Mocha and Chai.
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"@types/mathjs": "~4.4",
"@types/mocha": "~5.2",
"@types/node-emoji": "~1.8",
"@types/passport": "^0.4.7",
"@types/passport-jwt": "^3.0.1",
"@types/request": "~2.48",
"@types/request-promise": "~4.1",
"@types/request-promise-native": "~1.0",
Expand Down Expand Up @@ -63,6 +65,8 @@
"node-emoji": "~1.8",
"noicejs": "~2.3.3",
"nyc": "~13.1",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"prom-client": "^11.2.0",
"raw-loader": "^0.5.1",
"reflect-metadata": "^0.1.0",
Expand Down
4 changes: 3 additions & 1 deletion src/BaseService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { kebabCase } from 'lodash';
import { Logger } from 'noicejs/logger/Logger';
import * as uuid from 'uuid/v4';

import { Service, ServiceOptions } from 'src/Service';

import { dictToMap } from './utils/Map';

export abstract class BaseService<TData> implements Service {
Expand All @@ -27,7 +29,7 @@ export abstract class BaseService<TData> implements Service {
}

this.logger = options.logger.child({
class: Reflect.getPrototypeOf(this).constructor.name,
kind: kebabCase(Reflect.getPrototypeOf(this).constructor.name),
service: options.metadata.name,
});
}
Expand Down
12 changes: 8 additions & 4 deletions src/Bot.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { bindAll } from 'lodash';
import { Container, Inject } from 'noicejs';
import { Container, Inject, MissingValueError } from 'noicejs';
import { BaseOptions } from 'noicejs/Container';
import { Logger, LogLevel } from 'noicejs/logger/Logger';
import { collectDefaultMetrics, Counter, Registry } from 'prom-client';
Expand Down Expand Up @@ -135,6 +135,10 @@ export class Bot extends BaseService<BotData> implements Service {
public async receive(msg: Message) {
this.logger.debug({ msg }, 'received incoming message');

if (!msg.context.name || !msg.context.uid) {
throw new MissingValueError('msg context name and uid required');
}

if (!await this.checkFilters(msg)) {
this.logger.warn({ msg }, 'dropped incoming message due to filters');
return;
Expand All @@ -145,10 +149,9 @@ export class Bot extends BaseService<BotData> implements Service {
try {
if (await parser.match(msg)) {
matched = true;
this.logger.debug({ msg, parser: parser.name }, 'parsing message');
const commands = await parser.parse(msg);
for (const cmd of commands) {
this.commands.next(cmd);
}
this.emitCommand(...commands);
}
} catch (err) {
this.logger.error(err, 'error running parser');
Expand Down Expand Up @@ -187,6 +190,7 @@ export class Bot extends BaseService<BotData> implements Service {
const results = [];
for (const data of messages) {
const msg = await this.storage.getRepository(Message).save(data);
this.logger.debug({ msg }, 'message saved');
this.outgoing.next(msg);
results.push(msg);
}
Expand Down
97 changes: 15 additions & 82 deletions src/controller/AuthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { Inject } from 'noicejs';
import { Connection, Repository } from 'typeorm';

import { Role } from 'src/entity/auth/Role';
import { Session } from 'src/entity/auth/Session';
import { Token } from 'src/entity/auth/Token';
import { User } from 'src/entity/auth/User';
import { Command, CommandVerb } from 'src/entity/Command';
import { Context, ContextData } from 'src/entity/Context';
import { Context } from 'src/entity/Context';
import { Message } from 'src/entity/Message';
import { TYPE_JSON, TYPE_TEXT } from 'src/utils/Mime';
import { SessionProvider } from 'src/utils/SessionProvider';

import { BaseController } from './BaseController';
import { Controller, ControllerData, ControllerOptions } from './Controller';
Expand All @@ -21,10 +20,10 @@ export type AuthControllerData = ControllerData;
export type AuthControllerOptions = ControllerOptions<AuthControllerData>;

@Inject('bot', 'storage')
export class AuthController extends BaseController<AuthControllerData> implements Controller, SessionProvider {
export class AuthController extends BaseController<AuthControllerData> implements Controller {
protected storage: Connection;
protected roleRepository: Repository<Role>;
protected sessionRepository: Repository<Session>;
protected tokenRepository: Repository<Token>;
protected userRepository: Repository<User>;

constructor(options: AuthControllerOptions) {
Expand All @@ -35,7 +34,7 @@ export class AuthController extends BaseController<AuthControllerData> implement

this.storage = options.storage;
this.roleRepository = this.storage.getRepository(Role);
this.sessionRepository = this.storage.getRepository(Session);
this.tokenRepository = this.storage.getRepository(Token);
this.userRepository = this.storage.getRepository(User);
}

Expand Down Expand Up @@ -73,16 +72,10 @@ export class AuthController extends BaseController<AuthControllerData> implement
}

public async createUser(cmd: Command): Promise<void> {
if (cmd.context.session) {
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot create users while logged in'));
return;
}

const name = cmd.getHeadOrDefault('name', cmd.context.userName);
const roles = cmd.get('roles');
const name = cmd.getHeadOrDefault('name', cmd.context.name);
const user = await this.userRepository.save(this.userRepository.create({
name,
roles,
roles: [],
}));

this.logger.debug({ user }, 'created user');
Expand All @@ -91,70 +84,40 @@ export class AuthController extends BaseController<AuthControllerData> implement
}

public async createSession(cmd: Command): Promise<void> {
if (cmd.context.session) {
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot create sessions while logged in'));
return;
}

const sessionKey = AuthController.getSessionKey(cmd.context);
const existingSession = await this.sessionRepository.findOne(sessionKey);
if (existingSession) {
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'session already exists'));
return;
}

const userName = cmd.getHeadOrDefault('name', cmd.context.userName);
const userName = cmd.getHeadOrDefault('name', cmd.context.name);
const user = await this.userRepository.findOne({
name: userName,
});

if (isNil(user)) {
this.logger.warn({ sessionKey, userName }, 'user not found for new session');
this.logger.warn({ userName }, 'user not found for new session');
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'user not found'));
return;
}

this.logger.debug({ user }, 'logging in user');

const session = await this.sessionRepository.save(this.sessionRepository.create({
...AuthController.getSessionKey(cmd.context),
user,
}));
const session = await cmd.context.source.createSession(cmd.context.uid, user);

this.logger.debug({ session, user, userName }, 'created session');
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'created session'));
return;
}

public async getUser(cmd: Command): Promise<void> {
if (!cmd.context.session) {
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot get users unless logged in'));
return;
}

const session = await this.sessionRepository.findOne({
id: cmd.context.session.id,
});
if (isNil(session)) {
const { token } = cmd.context;
if (isNil(token)) {
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'session does not exist'));
return;
}

await this.bot.sendMessage(Message.reply(cmd.context, TYPE_JSON, session.user.toString()));
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_JSON, token.user.name));
return;
}

public async getSession(cmd: Command): Promise<void> {
if (!cmd.context.session) {
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot get sessions unless logged in'));
return;
}

const session = await this.sessionRepository.findOne({
id: cmd.context.session.id,
});
const session = cmd.context.source.getSession(cmd.context.uid);
if (isNil(session)) {
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'session does not exist'));
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot get sessions unless logged in'));
return;
}

Expand All @@ -167,34 +130,4 @@ export class AuthController extends BaseController<AuthControllerData> implement
public async checkPermissions(ctx: Context, perms: Array<string>): Promise<boolean> {
return false;
}

/**
* Attach session information to the provided context.
*/
public async createSessionContext(data: ContextData): Promise<Context> {
this.logger.debug({ data }, 'decorating context with session');

const sessionKey = AuthController.getSessionKey(data);
const session = await this.sessionRepository.findOne(sessionKey);

if (isNil(session)) {
this.logger.debug({ data }, 'no session for context');
return new Context(data);
}

const context = new Context({
...data,
session,
});
this.logger.debug({ context, session }, 'found session for context');

return context;
}

protected static getSessionKey(ctx: ContextData) {
return {
listenerId: ctx.listenerId,
userName: ctx.userId,
};
}
}
6 changes: 3 additions & 3 deletions src/controller/CountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ export class CountController extends BaseController<CountControllerData> impleme

public async handle(cmd: Command): Promise<void> {
const count = cmd.getHeadOrDefault(this.data.field.count, this.data.default.count);
const name = cmd.getHeadOrDefault(this.data.field.name, cmd.context.threadId);
const name = cmd.getHeadOrDefault(this.data.field.name, cmd.context.channel.thread);

this.logger.debug({ count, counterName: name }, 'finding counter');
const counter = await this.findOrCreateCounter(name, cmd.context.roomId);
const counter = await this.findOrCreateCounter(name, cmd.context.channel.id);

switch (count) {
case 'ls':
const body = await this.listCounters(cmd.context.roomId);
const body = await this.listCounters(cmd.context.channel.id);
await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, body));
break;
case '++':
Expand Down
6 changes: 3 additions & 3 deletions src/controller/SedController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export class SedController extends BaseController<SedControllerData> implements
let messages: Array<Message> = [];
try {
messages = await this.bot.fetch({
channel: cmd.context.roomId,
listenerId: cmd.context.listenerId,
channel: cmd.context.channel.id,
listenerId: cmd.context.source.id,
useFilters: true,
});
} catch (error) {
Expand All @@ -52,7 +52,7 @@ export class SedController extends BaseController<SedControllerData> implements
}

private async processMessage(message: Message, command: Command, parts: RegExpMatchArray): Promise<boolean> {
if (message.context.threadId === command.context.threadId) {
if (message.context.channel.thread === command.context.channel.thread) {
return false;
}

Expand Down
Loading

0 comments on commit dc0b6b1

Please sign in to comment.