Implement basic login & registration forms
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
5940b6e2b3
commit
a1d04d652e
@ -47,9 +47,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"prebuild": "pnpm run lint && rimraf lib",
|
"build": "pnpm run lint && rimraf lib && tsc && fse copy --all --dereference --preserveTimestamps --keepExisting=false --quiet --errorOnExist=false src/resources lib/resources",
|
||||||
"build": "tsc",
|
|
||||||
"postbuild": "fse copy --all --dereference --preserveTimestamps --keepExisting=false --quiet --errorOnExist=false src/resources lib/resources",
|
|
||||||
"app": "tsc && node lib/index.js",
|
"app": "tsc && node lib/index.js",
|
||||||
"prepare": "pnpm run build",
|
"prepare": "pnpm run build",
|
||||||
"docs:build": "typedoc --options typedoc.json",
|
"docs:build": "typedoc --options typedoc.json",
|
||||||
|
5
src/auth/AuthenticatableAlreadyExistsError.ts
Normal file
5
src/auth/AuthenticatableAlreadyExistsError.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import {ErrorWithContext} from '../util'
|
||||||
|
|
||||||
|
export class AuthenticatableAlreadyExistsError extends ErrorWithContext {
|
||||||
|
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import {Inject, Injectable} from '../di'
|
import {Inject, Injectable} from '../di'
|
||||||
import {EventBus} from '../event/EventBus'
|
import {EventBus} from '../event/EventBus'
|
||||||
import {Awaitable, Maybe} from '../util'
|
import {Awaitable, Maybe} from '../util'
|
||||||
import {Authenticatable, AuthenticatableRepository} from './types'
|
import {Authenticatable, AuthenticatableCredentials, AuthenticatableRepository} from './types'
|
||||||
import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent'
|
import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent'
|
||||||
import {UserFlushedEvent} from './event/UserFlushedEvent'
|
import {UserFlushedEvent} from './event/UserFlushedEvent'
|
||||||
import {UserAuthenticationResumedEvent} from './event/UserAuthenticationResumedEvent'
|
import {UserAuthenticationResumedEvent} from './event/UserAuthenticationResumedEvent'
|
||||||
|
import {Logging} from '../service/Logging'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base-class for a context that authenticates users and manages security.
|
* Base-class for a context that authenticates users and manages security.
|
||||||
@ -14,6 +15,9 @@ export abstract class SecurityContext {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly bus!: EventBus
|
protected readonly bus!: EventBus
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
/** The currently authenticated user, if one exists. */
|
/** The currently authenticated user, if one exists. */
|
||||||
private authenticatedUser?: Authenticatable
|
private authenticatedUser?: Authenticatable
|
||||||
|
|
||||||
@ -57,7 +61,7 @@ export abstract class SecurityContext {
|
|||||||
* unauthenticated implicitly.
|
* unauthenticated implicitly.
|
||||||
* @param credentials
|
* @param credentials
|
||||||
*/
|
*/
|
||||||
async attemptOnce(credentials: Record<string, string>): Promise<Maybe<Authenticatable>> {
|
async attemptOnce(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
||||||
const user = await this.repository.getByCredentials(credentials)
|
const user = await this.repository.getByCredentials(credentials)
|
||||||
if ( user ) {
|
if ( user ) {
|
||||||
await this.authenticateOnce(user)
|
await this.authenticateOnce(user)
|
||||||
@ -71,7 +75,7 @@ export abstract class SecurityContext {
|
|||||||
* authentication will be persisted.
|
* authentication will be persisted.
|
||||||
* @param credentials
|
* @param credentials
|
||||||
*/
|
*/
|
||||||
async attempt(credentials: Record<string, string>): Promise<Maybe<Authenticatable>> {
|
async attempt(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
||||||
const user = await this.repository.getByCredentials(credentials)
|
const user = await this.repository.getByCredentials(credentials)
|
||||||
if ( user ) {
|
if ( user ) {
|
||||||
await this.authenticate(user)
|
await this.authenticate(user)
|
||||||
@ -108,6 +112,8 @@ export abstract class SecurityContext {
|
|||||||
*/
|
*/
|
||||||
async resume(): Promise<void> {
|
async resume(): Promise<void> {
|
||||||
const credentials = await this.getCredentials()
|
const credentials = await this.getCredentials()
|
||||||
|
this.logging.debug('resume:')
|
||||||
|
this.logging.debug(credentials)
|
||||||
const user = await this.repository.getByCredentials(credentials)
|
const user = await this.repository.getByCredentials(credentials)
|
||||||
if ( user ) {
|
if ( user ) {
|
||||||
this.authenticatedUser = user
|
this.authenticatedUser = user
|
||||||
@ -125,7 +131,7 @@ export abstract class SecurityContext {
|
|||||||
* Get the credentials for the current user from whatever storage medium
|
* Get the credentials for the current user from whatever storage medium
|
||||||
* the context's host provides.
|
* the context's host provides.
|
||||||
*/
|
*/
|
||||||
abstract getCredentials(): Awaitable<Record<string, string>>
|
abstract getCredentials(): Awaitable<AuthenticatableCredentials>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the currently authenticated user, if one exists.
|
* Get the currently authenticated user, if one exists.
|
||||||
@ -138,6 +144,8 @@ export abstract class SecurityContext {
|
|||||||
* Returns true if there is a currently authenticated user.
|
* Returns true if there is a currently authenticated user.
|
||||||
*/
|
*/
|
||||||
hasUser(): boolean {
|
hasUser(): boolean {
|
||||||
|
this.logging.debug('hasUser?')
|
||||||
|
this.logging.debug(this.authenticatedUser)
|
||||||
return Boolean(this.authenticatedUser)
|
return Boolean(this.authenticatedUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,145 @@
|
|||||||
import {Controller} from '../../http/Controller'
|
import {Controller} from '../../http/Controller'
|
||||||
import {Injectable} from '../../di'
|
import {Inject, Injectable} from '../../di'
|
||||||
import {Route} from '../../http/routing/Route'
|
import {ResponseObject, Route} from '../../http/routing/Route'
|
||||||
import {Request} from '../../http/lifecycle/Request'
|
import {Request} from '../../http/lifecycle/Request'
|
||||||
import {view} from '../../http/response/ViewResponseFactory'
|
import {view} from '../../http/response/ViewResponseFactory'
|
||||||
import {ResponseFactory} from '../../http/response/ResponseFactory'
|
import {ResponseFactory} from '../../http/response/ResponseFactory'
|
||||||
|
import {SecurityContext} from '../SecurityContext'
|
||||||
|
import {BasicLoginFormRequest} from './BasicLoginFormRequest'
|
||||||
|
import {Routing} from '../../service/Routing'
|
||||||
|
import {Valid, ValidationError} from '../../forms'
|
||||||
|
import {AuthenticatableCredentials} from '../types'
|
||||||
|
import {BasicRegisterFormRequest} from './BasicRegisterFormRequest'
|
||||||
|
import {AuthenticatableAlreadyExistsError} from '../AuthenticatableAlreadyExistsError'
|
||||||
|
import {Session} from '../../http/session/Session'
|
||||||
|
import {temporary} from '../../http/response/TemporaryRedirectResponseFactory'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BasicLoginController extends Controller {
|
export class BasicLoginController extends Controller {
|
||||||
public static routes(): void {
|
public static routes({ enableRegistration = true } = {}): void {
|
||||||
Route.group('auth', () => {
|
Route.group('auth', () => {
|
||||||
Route.get('login', (request: Request) => {
|
Route.get('login', (request: Request) => {
|
||||||
const controller = <BasicLoginController> request.make(BasicLoginController)
|
const controller = <BasicLoginController> request.make(BasicLoginController)
|
||||||
return controller.getLogin()
|
return controller.getLogin()
|
||||||
})
|
})
|
||||||
|
.pre('@auth:guest')
|
||||||
.alias('@auth.login')
|
.alias('@auth.login')
|
||||||
|
|
||||||
Route.post('login', (request: Request) => {
|
Route.post('login', (request: Request) => {
|
||||||
const controller = <BasicLoginController> request.make(BasicLoginController)
|
const controller = <BasicLoginController> request.make(BasicLoginController)
|
||||||
return controller.getLogin()
|
return controller.attemptLogin()
|
||||||
})
|
})
|
||||||
|
.pre('@auth:guest')
|
||||||
.alias('@auth.login.attempt')
|
.alias('@auth.login.attempt')
|
||||||
|
|
||||||
|
Route.any('logout', (request: Request) => {
|
||||||
|
const controller = <BasicLoginController> request.make(BasicLoginController)
|
||||||
|
return controller.attemptLogout()
|
||||||
|
})
|
||||||
|
.pre('@auth:required')
|
||||||
|
.alias('@auth.logout')
|
||||||
|
|
||||||
|
if ( enableRegistration ) {
|
||||||
|
Route.get('register', (request: Request) => {
|
||||||
|
const controller = <BasicLoginController> request.make(BasicLoginController)
|
||||||
|
return controller.getRegistration()
|
||||||
|
})
|
||||||
|
.pre('@auth:guest')
|
||||||
|
.alias('@auth.register')
|
||||||
|
|
||||||
|
Route.post('register', (request: Request) => {
|
||||||
|
const controller = <BasicLoginController> request.make(BasicLoginController)
|
||||||
|
return controller.attemptRegister()
|
||||||
|
})
|
||||||
|
.pre('@auth:guest')
|
||||||
|
.alias('@auth.register.attempt')
|
||||||
|
}
|
||||||
|
}).pre('@auth:web')
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly security!: SecurityContext
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly routing!: Routing
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly session!: Session
|
||||||
|
|
||||||
|
public getLogin(): ResponseFactory {
|
||||||
|
return this.getLoginView()
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRegistration(): ResponseFactory {
|
||||||
|
return this.getRegistrationView()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async attemptLogin(): Promise<ResponseObject> {
|
||||||
|
const form = <BasicLoginFormRequest> this.request.make(BasicLoginFormRequest)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data: Valid<AuthenticatableCredentials> = await form.get()
|
||||||
|
const user = await this.security.attempt(data)
|
||||||
|
if ( user ) {
|
||||||
|
const intention = this.session.get('auth.intention', '/')
|
||||||
|
this.session.forget('auth.intention')
|
||||||
|
return temporary(intention)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getLoginView(['Invalid username/password.'])
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if ( e instanceof ValidationError ) {
|
||||||
|
return this.getLoginView(e.errors.all())
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async attemptLogout(): Promise<ResponseObject> {
|
||||||
|
await this.security.flush()
|
||||||
|
return this.getMessageView('You have been logged out.')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async attemptRegister(): Promise<ResponseObject> {
|
||||||
|
const form = <BasicRegisterFormRequest> this.request.make(BasicRegisterFormRequest)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data: Valid<AuthenticatableCredentials> = await form.get()
|
||||||
|
const user = await this.security.repository.createByCredentials(data)
|
||||||
|
await this.security.authenticate(user)
|
||||||
|
|
||||||
|
const intention = this.session.get('auth.intention', '/')
|
||||||
|
this.session.forget('auth.intention')
|
||||||
|
return temporary(intention)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if ( e instanceof ValidationError ) {
|
||||||
|
return this.getRegistrationView(e.errors.all())
|
||||||
|
} else if ( e instanceof AuthenticatableAlreadyExistsError ) {
|
||||||
|
return this.getRegistrationView(['A user with that username already exists.'])
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getLoginView(errors?: string[]): ResponseFactory {
|
||||||
|
return view('@extollo:auth:login', {
|
||||||
|
formAction: this.routing.getNamedPath('@auth.login.attempt').toRemote,
|
||||||
|
errors,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLogin(): ResponseFactory {
|
protected getRegistrationView(errors?: string[]): ResponseFactory {
|
||||||
return view('@extollo:auth:login')
|
return view('@extollo:auth:register', {
|
||||||
|
formAction: this.routing.getNamedPath('@auth.register.attempt').toRemote,
|
||||||
|
errors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getMessageView(message: string): ResponseFactory {
|
||||||
|
return view('@extollo:auth:message', {
|
||||||
|
message,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
import {FormRequest, ValidationRules} from '../../forms'
|
import {FormRequest, ValidationRules} from '../../forms'
|
||||||
import {Is, Str} from '../../forms/rules/rules'
|
import {Is, Str} from '../../forms/rules/rules'
|
||||||
import {Singleton} from '../../di'
|
import {Singleton} from '../../di'
|
||||||
|
import {AuthenticatableCredentials} from '../types'
|
||||||
export interface BasicLoginCredentials {
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class BasicLoginFormRequest extends FormRequest<BasicLoginCredentials> {
|
export class BasicLoginFormRequest extends FormRequest<AuthenticatableCredentials> {
|
||||||
protected getRules(): ValidationRules {
|
protected getRules(): ValidationRules {
|
||||||
return {
|
return {
|
||||||
username: [
|
identifier: [
|
||||||
Is.required,
|
Is.required,
|
||||||
Str.lengthMin(1),
|
Str.lengthMin(1),
|
||||||
],
|
],
|
||||||
password: [
|
credential: [
|
||||||
Is.required,
|
Is.required,
|
||||||
Str.lengthMin(1),
|
Str.lengthMin(1),
|
||||||
],
|
],
|
||||||
|
22
src/auth/basic-ui/BasicRegisterFormRequest.ts
Normal file
22
src/auth/basic-ui/BasicRegisterFormRequest.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {FormRequest, ValidationRules} from '../../forms'
|
||||||
|
import {Is, Str} from '../../forms/rules/rules'
|
||||||
|
import {Singleton} from '../../di'
|
||||||
|
import {AuthenticatableCredentials} from '../types'
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class BasicRegisterFormRequest extends FormRequest<AuthenticatableCredentials> {
|
||||||
|
protected getRules(): ValidationRules {
|
||||||
|
return {
|
||||||
|
identifier: [
|
||||||
|
Is.required,
|
||||||
|
Str.lengthMin(1),
|
||||||
|
Str.alphaNum,
|
||||||
|
],
|
||||||
|
credential: [
|
||||||
|
Is.required,
|
||||||
|
Str.lengthMin(8),
|
||||||
|
Str.confirmed,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import {SecurityContext} from '../SecurityContext'
|
|||||||
import {Inject, Injectable} from '../../di'
|
import {Inject, Injectable} from '../../di'
|
||||||
import {Session} from '../../http/session/Session'
|
import {Session} from '../../http/session/Session'
|
||||||
import {Awaitable} from '../../util'
|
import {Awaitable} from '../../util'
|
||||||
import {AuthenticatableRepository} from '../types'
|
import {AuthenticatableCredentials, AuthenticatableRepository} from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security context implementation that uses the session as storage.
|
* Security context implementation that uses the session as storage.
|
||||||
@ -19,9 +19,10 @@ export class SessionSecurityContext extends SecurityContext {
|
|||||||
super(repository, 'session')
|
super(repository, 'session')
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentials(): Awaitable<Record<string, string>> {
|
getCredentials(): Awaitable<AuthenticatableCredentials> {
|
||||||
return {
|
return {
|
||||||
securityIdentifier: this.session.get('extollo.auth.securityIdentifier'),
|
identifier: '',
|
||||||
|
credential: this.session.get('extollo.auth.securityIdentifier'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,11 +24,11 @@ export class ORMUser extends Model<ORMUser> implements Authenticatable {
|
|||||||
|
|
||||||
/** The user's first name. */
|
/** The user's first name. */
|
||||||
@Field(FieldType.varchar, 'first_name')
|
@Field(FieldType.varchar, 'first_name')
|
||||||
public firstName!: string
|
public firstName?: string
|
||||||
|
|
||||||
/** The user's last name. */
|
/** The user's last name. */
|
||||||
@Field(FieldType.varchar, 'last_name')
|
@Field(FieldType.varchar, 'last_name')
|
||||||
public lastName!: string
|
public lastName?: string
|
||||||
|
|
||||||
/** The hashed and salted password of the user. */
|
/** The hashed and salted password of the user. */
|
||||||
@Field(FieldType.varchar, 'password_hash')
|
@Field(FieldType.varchar, 'password_hash')
|
||||||
|
@ -1,13 +1,22 @@
|
|||||||
import {Authenticatable, AuthenticatableIdentifier, AuthenticatableRepository} from '../types'
|
import {
|
||||||
|
Authenticatable,
|
||||||
|
AuthenticatableCredentials,
|
||||||
|
AuthenticatableIdentifier,
|
||||||
|
AuthenticatableRepository,
|
||||||
|
} from '../types'
|
||||||
import {Awaitable, Maybe} from '../../util'
|
import {Awaitable, Maybe} from '../../util'
|
||||||
import {ORMUser} from './ORMUser'
|
import {ORMUser} from './ORMUser'
|
||||||
import {Injectable} from '../../di'
|
import {Container, Inject, Injectable} from '../../di'
|
||||||
|
import {AuthenticatableAlreadyExistsError} from '../AuthenticatableAlreadyExistsError'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A user repository implementation that looks up users stored in the database.
|
* A user repository implementation that looks up users stored in the database.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ORMUserRepository extends AuthenticatableRepository {
|
export class ORMUserRepository extends AuthenticatableRepository {
|
||||||
|
@Inject('injector')
|
||||||
|
protected readonly injector!: Container
|
||||||
|
|
||||||
/** Look up the user by their username. */
|
/** Look up the user by their username. */
|
||||||
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
||||||
return ORMUser.query<ORMUser>()
|
return ORMUser.query<ORMUser>()
|
||||||
@ -21,21 +30,36 @@ export class ORMUserRepository extends AuthenticatableRepository {
|
|||||||
* If username/password are specified, look up the user and verify the password.
|
* If username/password are specified, look up the user and verify the password.
|
||||||
* @param credentials
|
* @param credentials
|
||||||
*/
|
*/
|
||||||
async getByCredentials(credentials: Record<string, string>): Promise<Maybe<Authenticatable>> {
|
async getByCredentials(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
||||||
if ( credentials.securityIdentifier ) {
|
if ( !credentials.identifier && credentials.credential ) {
|
||||||
return ORMUser.query<ORMUser>()
|
return ORMUser.query<ORMUser>()
|
||||||
.where('username', '=', credentials.securityIdentifier)
|
.where('username', '=', credentials.credential)
|
||||||
.first()
|
.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( credentials.username && credentials.password ) {
|
if ( credentials.identifier && credentials.credential ) {
|
||||||
const user = await ORMUser.query<ORMUser>()
|
const user = await ORMUser.query<ORMUser>()
|
||||||
.where('username', '=', credentials.username)
|
.where('username', '=', credentials.identifier)
|
||||||
.first()
|
.first()
|
||||||
|
|
||||||
if ( user && await user.verifyPassword(credentials.password) ) {
|
if ( user && await user.verifyPassword(credentials.credential) ) {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createByCredentials(credentials: AuthenticatableCredentials): Promise<Authenticatable> {
|
||||||
|
if ( await this.getByCredentials(credentials) ) {
|
||||||
|
throw new AuthenticatableAlreadyExistsError(`Authenticatable already exists with credentials.`, {
|
||||||
|
identifier: credentials.identifier,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = <ORMUser> this.injector.make(ORMUser)
|
||||||
|
user.username = credentials.identifier
|
||||||
|
await user.setPassword(credentials.credential)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,11 @@ import {Awaitable, JSONState, Maybe, Rehydratable} from '../util'
|
|||||||
/** Value that can be used to uniquely identify a user. */
|
/** Value that can be used to uniquely identify a user. */
|
||||||
export type AuthenticatableIdentifier = string | number
|
export type AuthenticatableIdentifier = string | number
|
||||||
|
|
||||||
|
export interface AuthenticatableCredentials {
|
||||||
|
identifier: string,
|
||||||
|
credential: string,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for entities that can be authenticated.
|
* Base class for entities that can be authenticated.
|
||||||
*/
|
*/
|
||||||
@ -32,5 +37,7 @@ export abstract class AuthenticatableRepository {
|
|||||||
* Returns the user if the credentials are valid.
|
* Returns the user if the credentials are valid.
|
||||||
* @param credentials
|
* @param credentials
|
||||||
*/
|
*/
|
||||||
abstract getByCredentials(credentials: Record<string, string>): Awaitable<Maybe<Authenticatable>>
|
abstract getByCredentials(credentials: AuthenticatableCredentials): Awaitable<Maybe<Authenticatable>>
|
||||||
|
|
||||||
|
abstract createByCredentials(credentials: AuthenticatableCredentials): Awaitable<Authenticatable>
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {ValidationResult, ValidatorFunction} from './types'
|
import {ValidationResult, ValidatorFunction, ValidatorFunctionParams} from './types'
|
||||||
import {isJSON} from '../../util'
|
import {isJSON} from '../../util'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,6 +221,24 @@ function lengthMax(len: number): ValidatorFunction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator function that requires the input value to match a `${field}Confirm` field's value.
|
||||||
|
* @param fieldName
|
||||||
|
* @param inputValue
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
function confirmed(fieldName: string, inputValue: unknown, params: ValidatorFunctionParams): ValidationResult {
|
||||||
|
const confirmedFieldName = `${fieldName}Confirm`
|
||||||
|
if ( inputValue === params.data[confirmedFieldName] ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `confirmation does not match`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const Str = {
|
export const Str = {
|
||||||
alpha,
|
alpha,
|
||||||
alphaNum,
|
alphaNum,
|
||||||
@ -242,4 +260,5 @@ export const Str = {
|
|||||||
length,
|
length,
|
||||||
lengthMin,
|
lengthMin,
|
||||||
lengthMax,
|
lengthMax,
|
||||||
|
confirmed,
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,11 @@ export default class CreateUsersTableMigration extends Migration {
|
|||||||
|
|
||||||
table.column('first_name')
|
table.column('first_name')
|
||||||
.type(FieldType.varchar)
|
.type(FieldType.varchar)
|
||||||
.required()
|
.nullable()
|
||||||
|
|
||||||
table.column('last_name')
|
table.column('last_name')
|
||||||
.type(FieldType.varchar)
|
.type(FieldType.varchar)
|
||||||
.required()
|
.nullable()
|
||||||
|
|
||||||
table.column('password_hash')
|
table.column('password_hash')
|
||||||
.type(FieldType.text)
|
.type(FieldType.text)
|
||||||
|
@ -612,6 +612,8 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
|||||||
}
|
}
|
||||||
|
|
||||||
const row = this.buildInsertFieldObject()
|
const row = this.buildInsertFieldObject()
|
||||||
|
this.logging.debug('Insert field object:')
|
||||||
|
this.logging.debug(row)
|
||||||
const returnable = new Collection<string>([this.keyName(), ...Object.keys(row)])
|
const returnable = new Collection<string>([this.keyName(), ...Object.keys(row)])
|
||||||
|
|
||||||
const result = await this.query()
|
const result = await this.query()
|
||||||
@ -808,10 +810,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
|||||||
private buildInsertFieldObject(): EscapeValueObject {
|
private buildInsertFieldObject(): EscapeValueObject {
|
||||||
const ctor = this.constructor as typeof Model
|
const ctor = this.constructor as typeof Model
|
||||||
|
|
||||||
|
this.logging.debug(`buildInsertFieldObject populateKeyOnInsert? ${ctor.populateKeyOnInsert}; keyName: ${this.keyName()}`)
|
||||||
|
|
||||||
return getFieldsMeta(this)
|
return getFieldsMeta(this)
|
||||||
.pipe()
|
.pipe()
|
||||||
.unless(ctor.populateKeyOnInsert, fields => {
|
.unless(ctor.populateKeyOnInsert, fields => {
|
||||||
return fields.where('modelKey', '!=', this.keyName())
|
return fields.where('databaseKey', '!=', this.keyName())
|
||||||
})
|
})
|
||||||
.get()
|
.get()
|
||||||
.keyMap('databaseKey', inst => (this as any)[inst.modelKey])
|
.keyMap('databaseKey', inst => (this as any)[inst.modelKey])
|
||||||
|
@ -164,12 +164,8 @@ export class PostgresSchema extends Schema {
|
|||||||
.pluck<string>('column_name')
|
.pluck<string>('column_name')
|
||||||
.each(col => idx.field(col))
|
.each(col => idx.field(col))
|
||||||
})
|
})
|
||||||
.when(groupedIndexes[key]?.[0]?.indisprimary, idx => {
|
.when(groupedIndexes[key]?.[0]?.indisprimary, idx => idx.primary())
|
||||||
idx.primary()
|
.when(groupedIndexes[key]?.[0]?.indisunique, idx => idx.unique())
|
||||||
})
|
|
||||||
.when(groupedIndexes[key]?.[0]?.indisunique, idx => {
|
|
||||||
idx.unique()
|
|
||||||
})
|
|
||||||
.get()
|
.get()
|
||||||
.flagAsExistingInSchema()
|
.flagAsExistingInSchema()
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,11 @@ import {FieldType} from '../types'
|
|||||||
* Model used to fetch & store sessions from the ORMSession driver.
|
* Model used to fetch & store sessions from the ORMSession driver.
|
||||||
*/
|
*/
|
||||||
export class SessionModel extends Model<SessionModel> {
|
export class SessionModel extends Model<SessionModel> {
|
||||||
protected static table = 'sessions'; // FIXME allow configuring
|
protected static table = 'sessions' // FIXME allow configuring
|
||||||
|
|
||||||
protected static key = 'session_uuid';
|
protected static key = 'session_uuid'
|
||||||
|
|
||||||
|
protected static populateKeyOnInsert = true
|
||||||
|
|
||||||
@Field(FieldType.varchar, 'session_uuid')
|
@Field(FieldType.varchar, 'session_uuid')
|
||||||
public uuid!: string;
|
public uuid!: string;
|
||||||
|
@ -8,5 +8,9 @@ block content
|
|||||||
each error in errors
|
each error in errors
|
||||||
p.form-error-message #{error}
|
p.form-error-message #{error}
|
||||||
|
|
||||||
form(method='post' enctype='multipart/form-data')
|
if formAction
|
||||||
block form
|
form(method='post' enctype='multipart/form-data' action=formAction)
|
||||||
|
block form
|
||||||
|
else
|
||||||
|
form(method='post' enctype='multipart/form-data')
|
||||||
|
block form
|
||||||
|
@ -8,11 +8,11 @@ block heading
|
|||||||
|
|
||||||
block form
|
block form
|
||||||
.form-label-group
|
.form-label-group
|
||||||
input#inputUsername.form-control(type='text' name='username' value=(formData ? formData.username : '') required placeholder='Username' autofocus)
|
input#inputUsername.form-control(type='text' name='identifier' value=(formData ? formData.username : '') required placeholder='Username' autofocus)
|
||||||
label(for='inputUsername') Username
|
label(for='inputUsername') Username
|
||||||
|
|
||||||
.form-label-group
|
.form-label-group
|
||||||
input#inputPassword.form-control(type='password' name='password' required placeholder='Password')
|
input#inputPassword.form-control(type='password' name='credential' required placeholder='Password')
|
||||||
label(for='inputPassword') Password
|
label(for='inputPassword') Password
|
||||||
|
|
||||||
|
|
||||||
@ -21,4 +21,4 @@ block form
|
|||||||
|
|
||||||
.text-center
|
.text-center
|
||||||
span.small Need an account?
|
span.small Need an account?
|
||||||
a(href='./register') Register here.
|
a(href=named('@auth.register')) Register here.
|
||||||
|
26
src/resources/views/auth/register.pug
Normal file
26
src/resources/views/auth/register.pug
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
extends ./form
|
||||||
|
|
||||||
|
block head
|
||||||
|
title Register | #{config('app.name', 'Extollo')}
|
||||||
|
|
||||||
|
block heading
|
||||||
|
| Register to continue
|
||||||
|
|
||||||
|
block form
|
||||||
|
.form-label-group
|
||||||
|
input#inputUsername.form-control(type='text' name='identifier' value=(formData ? formData.username : '') required placeholder='Username' autofocus)
|
||||||
|
label(for='inputUsername') Username
|
||||||
|
|
||||||
|
.form-label-group
|
||||||
|
input#inputPassword.form-control(type='password' name='credential' required placeholder='Password')
|
||||||
|
label(for='inputPassword') Password
|
||||||
|
|
||||||
|
.form-label-group
|
||||||
|
input#inputPasswordConfirm.form-control(type='password' name='credentialConfirm' required placeholder='Confirm Password')
|
||||||
|
label(for='inputPassword') Confirm Password
|
||||||
|
|
||||||
|
button.btn.btn-lg.btn-primary.btn-block.btn-login.text-uppercase.font-weight-bold.mb-2.form-submit-button(type='submit') Login
|
||||||
|
|
||||||
|
.text-center
|
||||||
|
span.small Have an account?
|
||||||
|
a(href=named('@auth.login')) Login here.
|
@ -82,7 +82,8 @@ export class HTTPServer extends Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get handler(): RequestListener {
|
public get handler(): RequestListener {
|
||||||
const timeout = this.config.get('server.timeout', 10000)
|
// const timeout = this.config.get('server.timeout', 10000)
|
||||||
|
const timeout = 0 // temporarily disable this because it is causing problems
|
||||||
|
|
||||||
return async (request: IncomingMessage, response: ServerResponse) => {
|
return async (request: IncomingMessage, response: ServerResponse) => {
|
||||||
const extolloReq = new Request(request, response)
|
const extolloReq = new Request(request, response)
|
||||||
|
@ -8,7 +8,7 @@ export type PipeOperator<T, T2> = (subject: T) => T2
|
|||||||
/**
|
/**
|
||||||
* A closure that maps a given pipe item to an item of the same type.
|
* A closure that maps a given pipe item to an item of the same type.
|
||||||
*/
|
*/
|
||||||
export type ReflexivePipeOperator<T> = (subject: T) => T|void
|
export type ReflexivePipeOperator<T> = (subject: T) => T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A condition or condition-resolving function for pipe methods.
|
* A condition or condition-resolving function for pipe methods.
|
||||||
@ -97,7 +97,7 @@ export class Pipe<T> {
|
|||||||
*/
|
*/
|
||||||
when(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
|
when(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
|
||||||
if ( (typeof check === 'function' && check(this.subject)) || check ) {
|
if ( (typeof check === 'function' && check(this.subject)) || check ) {
|
||||||
Pipe.wrap(op(this.subject))
|
return Pipe.wrap(op(this.subject))
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
@ -115,8 +115,7 @@ export class Pipe<T> {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
Pipe.wrap(op(this.subject))
|
return Pipe.wrap(op(this.subject))
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,12 +52,14 @@ export function withTimeout<T>(timeout: number, promise: Promise<T>): TimeoutSub
|
|||||||
run: async () => {
|
run: async () => {
|
||||||
let expired = false
|
let expired = false
|
||||||
let resolved = false
|
let resolved = false
|
||||||
setTimeout(() => {
|
if ( timeout ) {
|
||||||
expired = true
|
setTimeout(() => {
|
||||||
if ( !resolved ) {
|
expired = true
|
||||||
timeoutHandler()
|
if ( !resolved ) {
|
||||||
}
|
timeoutHandler()
|
||||||
}, timeout)
|
}
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
const result: T = await promise
|
const result: T = await promise
|
||||||
resolved = true
|
resolved = true
|
||||||
|
@ -41,7 +41,7 @@ export class PugViewEngine extends ViewEngine {
|
|||||||
return {
|
return {
|
||||||
basedir: templateName ? this.resolveBasePath(templateName).toLocal : this.path.toLocal,
|
basedir: templateName ? this.resolveBasePath(templateName).toLocal : this.path.toLocal,
|
||||||
debug: this.debug,
|
debug: this.debug,
|
||||||
compileDebug: this.debug,
|
// compileDebug: this.debug,
|
||||||
globals: [],
|
globals: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user