Start basic model & api scaffolding
This commit is contained in:
parent
95e897540d
commit
6eede45f7b
1006
pnpm-lock.yaml
Normal file
1006
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
47
src/app/http/controllers/api/v1/Relay.controller.ts
Normal file
47
src/app/http/controllers/api/v1/Relay.controller.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
58
src/app/http/controllers/auth/Login.controller.ts
Normal file
58
src/app/http/controllers/auth/Login.controller.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
30
src/app/http/middlewares/auth/InjectUser.middleware.ts
Normal file
30
src/app/http/middlewares/auth/InjectUser.middleware.ts
Normal 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.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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')
|
||||||
|
32
src/app/models/AccessToken.model.ts
Normal file
32
src/app/models/AccessToken.model.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
70
src/app/models/LoginToken.model.ts
Normal file
70
src/app/models/LoginToken.model.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
42
src/app/models/ServerSentRequest.model.ts
Normal file
42
src/app/models/ServerSentRequest.model.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
23
src/app/models/msg/Thread.model.ts
Normal file
23
src/app/models/msg/Thread.model.ts
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user