Start basic model & api scaffolding

This commit is contained in:
Garrett Mills 2021-05-15 23:18:37 -05:00
parent 95e897540d
commit 6eede45f7b
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
11 changed files with 1372 additions and 3 deletions

1006
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
import {Controller, HTTPError, json} from '@extollo/lib'
import {Inject, Injectable} from '@extollo/di'
import {User} from '../../../../models/User.model'
import {ServerSentRequest} from '../../../../models/ServerSentRequest.model'
import {HTTPStatus} from "@extollo/util";
/**
* Relay Controller
*/
@Injectable()
export class Relay extends Controller {
@Inject()
protected readonly user!: User
public async getRequestQueue() {
const requests = await ServerSentRequest.query<ServerSentRequest>()
.where('serviced', '=', false)
.where('user_id', '=', this.user.key())
.orderByAscending((new ServerSentRequest()).keyName())
.get()
.map(request => request.toObject())
return json(requests.toArray())
}
public async serviceRequest() {
const required = ['server_request_id', 'response_data']
for ( const field of required ) {
if ( !this.request.input(field) ) {
throw new HTTPError(HTTPStatus.http400, `Missing field: ${field}`)
}
}
const request = await ServerSentRequest.query<ServerSentRequest>()
.where('serviced', '=', false)
.where((new ServerSentRequest()).keyName(), '=', this.request.input('server_request_id'))
.first()
if ( !request ) {
throw new HTTPError(HTTPStatus.http404, 'Invalid request ID.')
}
request.serviced = true
request.responseData = this.request.input('response_data')
await request.save()
}
}

View File

@ -0,0 +1,58 @@
import {Controller, HTTPError, json, Session} from '@extollo/lib'
import {Inject, Injectable} from '@extollo/di'
import {User} from '../../../models/User.model'
import {HTTPStatus} from '@extollo/util'
import {LoginToken} from '../../../models/LoginToken.model'
/**
* Login Controller
*/
@Injectable()
export class Login extends Controller {
@Inject()
protected readonly session!: Session
public async debugInjectUser() {
const user = await User.query<User>().get().first()
if ( user ) {
this.session.set('auth.user_id', user.key())
return json(user)
}
return json({
success: false,
error: 'No user found.',
})
}
public async getLoginToken() {
if ( !this.request.hasKey(User) ) {
throw new HTTPError(HTTPStatus.FORBIDDEN)
}
const user = this.request.make<User>(User)
const token = await LoginToken.forUser(user)
return json(token.toObject())
}
public async redeemToken() {
const tokenValue = this.request.input('token')
if ( !tokenValue || typeof tokenValue !== 'string' ) {
throw new HTTPError(HTTPStatus.http400, 'Invalid or missing token value.')
}
const loginToken = await LoginToken.query<LoginToken>()
.where('token', '=', tokenValue)
.where('redeemed', '=', false)
.first()
if ( !loginToken ) {
throw new HTTPError(HTTPStatus.NOT_FOUND, 'Invalid token value.')
}
const accessToken = await loginToken.redeem()
return json({
token: accessToken.token
})
}
}

View File

@ -0,0 +1,30 @@
import {Logging, Middleware, Session} from "@extollo/lib"
import {Inject, Injectable} from "@extollo/di"
import {User} from "../../../models/User.model";
/**
* InjectUser Middleware
* --------------------------------------------
* Put some description here.
*/
@Injectable()
export class InjectUser extends Middleware {
@Inject()
protected readonly session!: Session
@Inject()
protected readonly logging!: Logging
public async apply() {
const userId = this.session.get('auth.user_id')
if ( userId && !this.request.hasKey(User) ) {
const user = await User.findByKey<User>(userId)
this.logging.debug(`Looked up user ID ${userId} from session. Found? ${!!user}`)
if ( user ) {
this.request.registerSingletonInstance<User>(User, user)
}
} else {
this.logging.verbose(`No user ID defined in session.`)
}
}
}

View File

@ -0,0 +1,38 @@
import {HTTPError, Middleware} from '@extollo/lib'
import {Injectable} from '@extollo/di'
import {AccessToken} from '../../../models/AccessToken.model'
import {HTTPStatus} from '@extollo/util'
import {User} from '../../../models/User.model'
/**
* ValidateAccessToken Middleware
*/
@Injectable()
export class ValidateAccessToken extends Middleware {
public async apply() {
const tokenValue = this.request.getHeader('X-Hyperlink-Access-Token')
|| this.request.input('x_hyperlink_access_token')
if ( !tokenValue ) {
throw new HTTPError(HTTPStatus.FORBIDDEN, 'Missing access token.')
}
const token = await AccessToken.query<AccessToken>()
.where('active', '=', true)
.where('token', '=', tokenValue)
.first()
if ( !token ) {
throw new HTTPError(HTTPStatus.FORBIDDEN, 'Invalid access token.')
}
const user = await token.user()
if ( !user ) {
throw new HTTPError(HTTPStatus.FORBIDDEN, 'Invalid access token.')
}
if ( !this.request.hasKey(User) ) {
this.request.registerSingletonInstance<User>(User, user)
}
}
}

View File

@ -1,3 +1,17 @@
import {Route} from "@extollo/lib" import {Route} from "@extollo/lib"
Route.get('/', 'main:Home.welcome') Route.get('/', 'main:Home.welcome')
Route.group('/api/v1', () => {
Route.any('/login/redeem', 'auth:Login.redeemToken')
Route.group('', () => {
Route.get('/request/queue', 'api:v1:Relay.getRequestQueue')
Route.post('/request/service', 'api:v1:Relay.serviceRequest')
}).pre('auth:ValidateAccessToken')
})
Route.group('/debug', () => {
Route.get('/inject-user', 'auth:Login.debugInjectUser')
Route.get('/login-token', 'auth:Login.getLoginToken')
}).pre('auth:InjectUser')

View File

@ -0,0 +1,32 @@
import {Field, FieldType, Model} from '@extollo/orm'
import {Injectable} from '@extollo/di'
import {User} from './User.model'
/**
* AccessToken Model
*/
@Injectable()
export class AccessToken extends Model<AccessToken> {
protected static table = 'access_token'
protected static key = 'access_token_id'
@Field(FieldType.serial)
public access_token_id!: number
@Field(FieldType.int4, 'user_id')
public userId!: number
@Field(FieldType.varchar)
public token!: string
@Field(FieldType.boolean)
public active!: boolean
user() {
return User.query<User>()
.where('user_id', '=', this.userId)
.limit(1)
.get()
.first()
}
}

View File

@ -0,0 +1,70 @@
import {uuid_v4} from '@extollo/util'
import {Field, FieldType, Model} from '@extollo/orm'
import {Injectable} from '@extollo/di'
import {AccessToken} from './AccessToken.model'
import {User} from './User.model'
/**
* LoginToken Model
*/
@Injectable()
export class LoginToken extends Model<LoginToken> {
public static async forUser(user: User): Promise<LoginToken> {
const token = new LoginToken()
const tokenValue = uuid_v4()
token.userId = user.key()
token.token = tokenValue
token.redeemed = false
await token.save()
// @ts-ignore
// FIXME - once @extollo/orm#3 is resolved, remove this
return LoginToken.query<LoginToken>()
.where('token', '=', tokenValue)
.first()
}
protected static table = 'login_token'
protected static key = 'login_token_id'
@Field(FieldType.serial)
public login_token_id!: number // FIXME when @extollo/orm#2 is fixed, change to camelCase
@Field(FieldType.int4, 'user_id')
public userId!: number
@Field(FieldType.varchar)
public token!: string
@Field(FieldType.boolean)
public redeemed!: boolean
user() {
return User.query<User>()
.where('user_id', '=', this.userId)
.limit(1)
.get()
.first()
}
async redeem(): Promise<AccessToken> {
const accessToken = new AccessToken()
const user = await this.user()
const tokenValue = (uuid_v4() + uuid_v4()).replace(/-/g, '')
accessToken.userId = user?.key()
accessToken.token = tokenValue
accessToken.active = true
await accessToken.save()
this.redeemed = true
await this.save()
// @ts-ignore
// FIXME - once @extollo/orm#3 is resolved, remove this
return AccessToken.query<AccessToken>()
.where('token', '=', tokenValue)
.first()
}
}

View File

@ -0,0 +1,42 @@
import {Field, FieldType, Model} from '@extollo/orm'
import {Injectable} from '@extollo/di'
import {User} from "./User.model";
export enum ServerRequestEndpoint {
LIST_THREADS = 'sre.threads.list'
}
/**
* ServerSentRequest Model
*/
@Injectable()
export class ServerSentRequest extends Model<ServerSentRequest> {
protected static table = 'server_request'
protected static key = 'server_request_id'
@Field(FieldType.serial)
public server_request_id!: number
@Field(FieldType.varchar)
public endpoint!: ServerRequestEndpoint
@Field(FieldType.int4, 'user_id')
public userId!: number
@Field(FieldType.boolean)
public serviced!: boolean
@Field(FieldType.json, 'request_data')
public requestData: any
@Field(FieldType.json, 'response_data')
public responseData: any
user() {
return User.query<User>()
.where('user_id', '=', this.userId)
.limit(1)
.get()
.first()
}
}

View File

@ -1,6 +1,15 @@
import {Model} from "@extollo/orm"; import {Field, FieldType, Model} from '@extollo/orm'
export class User extends Model<User> { export class User extends Model<User> {
protected static table = 'users'; protected static table = 'user'
protected static key = 'user_id'; protected static key = 'user_id'
@Field(FieldType.serial, 'user_id')
public userId!: number
@Field(FieldType.varchar)
public username!: string
@Field(FieldType.varchar, 'phone_number')
public phoneNumber!: string
} }

View File

@ -0,0 +1,23 @@
import {Field, FieldType, Model} from '@extollo/orm'
import {Injectable} from '@extollo/di'
/**
* Thread Model
*/
@Injectable()
export class Thread extends Model<Thread> {
protected static table = 'thread'
protected static key = 'thread_id'
@Field(FieldType.serial, 'thread_id')
public threadId!: number
@Field(FieldType.varchar, 'android_id')
public androidId!: string
@Field(FieldType.int4, 'user_id')
public userId!: number
@Field(FieldType.timestamp)
public timestamp!: Date
}