Last active
May 29, 2022 13:58
-
-
Save ollie314/6ad34c977ac70502491b6606b203b0cc to your computer and use it in GitHub Desktop.
Sample DDD implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a work in progress to support a workshop. | |
module Utils { | |
export namespace Duration { | |
export type DurationTime = number; | |
export type DurationUnit = | |
| "h" | |
| "hours" | |
| "m" | |
| "min" | |
| "mins" | |
| "minutes" | |
| "s" | |
| "sec" | |
| "secs" | |
| "seconds"; | |
export type Duration< | |
Time extends DurationTime = DurationTime, | |
Unit extends DurationUnit = DurationUnit | |
> = { | |
time: Time; | |
unit: Unit; | |
}; | |
export function normalizeDuration< | |
Time extends DurationTime = DurationTime, | |
Unit extends DurationUnit = DurationUnit | |
>(duration: Duration<Time, Unit>): Duration<Time, Unit> { | |
let op: number = 0; | |
switch (duration.unit) { | |
case "h": | |
case "hours": | |
op = 60 * 60; | |
break; | |
case "m": | |
case "minutes": | |
op = 60; | |
break; | |
case "s": | |
case "seconds": | |
op = 1; | |
break; | |
} | |
return { | |
time: (duration.time * op) as Time, | |
unit: "s" as Unit, | |
}; | |
} | |
} | |
export function pipe<A, B>(...fns: Function[]): (a: A) => B { | |
return (a: A) => (fns.reduce((acc, f) => f(acc), a) as unknown) as B; | |
} | |
} | |
module Iam { | |
// ================================================ // | |
// Model // | |
// ================================================ // | |
export type Username = string; | |
export type Password = string; | |
export type Email = string; | |
export type Identity = Username | Email; | |
export type Principal = { | |
credential: Identity; | |
password: Password; | |
}; | |
export type RemindUser = boolean; | |
export type Scope = string; | |
export type Scopes = Array<Scope>; | |
export type RoleId = string; | |
export type Role = { | |
id: RoleId; | |
name: string; | |
scopes: Scopes; | |
}; | |
export type Roles = Array<Role>; | |
// ================================================ // | |
// Identification // | |
// ================================================ // | |
// meaningfull constants | |
export const INVALID_USERNAME = "1"; | |
export const UNKNOWN_USER = "2"; | |
export const REMIND_USER = true; | |
export const DO_NOT_REMIND_USER = false; | |
// return type for the identification process | |
// Here is a pretty simple demonstration, a monadic type sounds far better here. | |
// Here we are synchronous for the sake of simplicity. | |
export type IdentificationSuccess = { | |
username: Username; | |
}; | |
export type IdentificationFailure = { | |
code: string; | |
message: string; | |
error?: Error; | |
}; | |
export type IdentificationResult = | |
| IdentificationSuccess | |
| IdentificationFailure; | |
export function identify(username: string): IdentificationResult { | |
if (username === "") { | |
return { | |
code: INVALID_USERNAME, | |
message: "The username must be provided", | |
}; | |
} | |
if (username === "root") { | |
return { | |
username: username as Username, | |
}; | |
} | |
return { | |
code: UNKNOWN_USER, | |
message: "The user does not exists", | |
}; | |
} | |
// ================================================ // | |
// Authentification // | |
// ================================================ // | |
// meaningfull constants | |
export const INVALID_PASSWORD = "3"; | |
export const INVALID_CREDENTIALS = "4"; | |
// return type for the authentication process | |
// Monadic type sounds far better here one more time and | |
// we are synchronous for the sake of simplicity. | |
export type AuthenticationSuccess = { | |
username: Username; | |
remindMe: boolean; | |
roles: Roles; | |
}; | |
export type AuthenticationFailure = { | |
code: string; | |
message: string; | |
}; | |
export type AuthenticationResult = | |
| AuthenticationSuccess | |
| AuthenticationFailure; | |
// identify function | |
export function authenticate(principal: Principal): AuthenticationResult; | |
export function authenticate( | |
principal: Principal, | |
remindMe: RemindUser | |
): AuthenticationResult; | |
export function authenticate( | |
username: Username, | |
password: Password | |
): AuthenticationResult; | |
export function authenticate( | |
username: Username, | |
password: Password, | |
remindMe: boolean | |
): AuthenticationResult; | |
export function authenticate( | |
a1: Principal | Username, | |
a2?: RemindUser | Password, | |
remindMe?: RemindUser | |
): AuthenticationResult { | |
let username: Username; | |
let password: Password; | |
let remindMe_ = DO_NOT_REMIND_USER; | |
if (typeof a1 === "string") { | |
if (!a2 || typeof a2 !== "string") { | |
return { | |
code: INVALID_PASSWORD, | |
message: "invalid password", | |
}; | |
} | |
username = a1; | |
password = a2; | |
if (remindMe !== undefined) { | |
remindMe_ = remindMe; | |
} | |
} else { | |
username = (a1 as Principal).credential; | |
password = (a1 as Principal).password; | |
if (a2 !== undefined) { | |
remindMe_ = a2 as boolean; | |
} | |
} | |
// a simple faker implementation of the control | |
if (username === "root" && password === "demodemo") { | |
return { | |
username, | |
remindMe: remindMe_, | |
roles: [ | |
{ | |
id: "1", | |
name: "root", | |
scopes: ["resources:read", "resources:write"], | |
}, | |
], | |
}; | |
} | |
return { | |
code: INVALID_CREDENTIALS, | |
message: "invalid credentials", | |
}; | |
} | |
// ================================================ // | |
// Authorization // | |
// ================================================ // | |
export type SessionDurationTime = Utils.Duration.DurationTime; | |
export type SessionDurationUnit = Utils.Duration.DurationUnit; | |
export type SessionDuration = Utils.Duration.Duration< | |
SessionDurationTime, | |
SessionDurationUnit | |
>; | |
export type SessionInfo = { | |
duration: SessionDuration; | |
}; | |
export type AuthorizationSuccess = { | |
username: Username; | |
roles: Roles; | |
sessionInfo: SessionInfo; | |
}; | |
export type AuthorizationFailure = { | |
code: string; | |
message: string; | |
error?: Error; | |
}; | |
export type AuthorizationResult = AuthorizationSuccess | AuthorizationFailure; | |
export function authorize( | |
username: Username, | |
userRoles: Roles, | |
acceptedScopes: Scopes | |
): AuthorizationResult { | |
const acceptScope = (s: Scope) => acceptedScopes.includes(s); | |
const scopes: Scopes = userRoles | |
.flatMap(({ scopes }) => scopes) | |
.filter(acceptScope); | |
if (scopes.length > 0) { | |
return { | |
username, | |
roles: userRoles.map((r) => ({ ...r, scopes })), | |
sessionInfo: { | |
duration: Utils.Duration.normalizeDuration({ time: 1, unit: "h" }), | |
}, | |
}; | |
} | |
return { | |
code: "-1", | |
message: "not implemented", | |
}; | |
} | |
// ================================================ // | |
// Workflows // | |
// ================================================ // | |
export type AuthenticationAccepted = { | |
username: Username; | |
remindMe: RemindUser; | |
roles: Roles; | |
sessionInfo: SessionInfo; | |
}; | |
export type AuthenticationRejected = | |
| IdentificationFailure | |
| AuthenticationFailure | |
| AuthorizationFailure; | |
export type AuthenticationWorkflowResult = | |
| AuthenticationAccepted | |
| AuthenticationRejected; | |
export type AuthenticateType = typeof authenticate; | |
export type AuthenticationWorkflowDependencies = { | |
identify: (username: Username) => IdentificationResult; | |
authenticate: AuthenticateType; | |
authorize: ( | |
username: Username, | |
userRoles: Roles, | |
acceptedScopes: Scopes | |
) => AuthorizationResult; | |
}; | |
export type AuthenticationWorkflow = ( | |
deps: AuthenticationWorkflowDependencies, | |
login: Username, | |
password: Password, | |
remindMe: RemindUser, | |
acceptedScopes: Scopes | |
) => AuthenticationWorkflowResult; | |
// get a partial application to generate the workflow | |
export const getAuthenticationWorkflow = ( | |
deps: AuthenticationWorkflowDependencies, | |
acceptedScopes: Scopes | |
) => { | |
type AuthenticationVector = { | |
login: Username; | |
password: Password; | |
remindMe: RemindUser; | |
}; | |
type IdentificationVector = { | |
login: Username; | |
password: Password; | |
remindMe: RemindUser; | |
}; | |
type IdentifiedVector = AuthenticationVector & IdentificationSuccess; | |
type AuthentifiedVector = IdentifiedVector & AuthenticationSuccess; | |
type AuthorizedVector = AuthentifiedVector & AuthorizationSuccess; | |
const toAuthenticationVector = ( | |
login: Username, | |
password: Password, | |
remindMe: RemindUser | |
): IdentificationVector => ({ | |
login, | |
password, | |
remindMe, | |
}); | |
const ident = (auth: AuthenticationVector) => { | |
const identified = deps.identify(auth.login); | |
if ((identified as IdentificationSuccess).username) { | |
return { | |
...auth, | |
...(identified as IdentificationSuccess), | |
}; | |
} else { | |
return identified as IdentificationFailure; | |
} | |
}; | |
const authe = (vect: IdentificationFailure | IdentifiedVector) => { | |
if ((vect as IdentificationFailure).code) { | |
return vect as IdentificationFailure; | |
} | |
const principal: Principal = { | |
credential: (vect as IdentifiedVector).username, | |
password: (vect as IdentifiedVector).password, | |
}; | |
const authenticated = deps.authenticate( | |
principal, | |
(vect as IdentifiedVector).remindMe | |
); | |
if ((authenticated as AuthenticationSuccess).username) { | |
return { | |
...(vect as IdentifiedVector), | |
...(authenticated as AuthenticationSuccess), | |
}; | |
} else { | |
return authenticated as AuthenticationFailure; | |
} | |
}; | |
const autho = (vect: AuthenticationFailure | AuthentifiedVector) => { | |
if ((vect as AuthenticationFailure).code) { | |
return vect as AuthenticationFailure; | |
} | |
const authorized = deps.authorize( | |
(vect as AuthentifiedVector).username, | |
(vect as AuthentifiedVector).roles, | |
acceptedScopes | |
); | |
if ((authorized as AuthorizationSuccess).username) { | |
return { | |
...(vect as AuthentifiedVector), | |
...(authorized as AuthorizationSuccess), | |
}; | |
} else { | |
return authorized as AuthorizationFailure; | |
} | |
}; | |
const normalize = (vect: AuthorizationFailure | AuthorizedVector) => { | |
if ((vect as AuthenticationFailure).code) { | |
return vect as AuthenticationFailure; | |
} | |
return { | |
username: (vect as AuthorizedVector).username, | |
remindMe: (vect as AuthorizedVector).remindMe, | |
roles: (vect as AuthorizedVector).roles, | |
sessionInfo: (vect as AuthorizedVector).sessionInfo, | |
} as AuthenticationWorkflowResult; | |
}; | |
return ( | |
login: Username, | |
password: Password, | |
remindMe: RemindUser | |
): AuthenticationWorkflowResult => { | |
const f = Utils.pipe<IdentificationVector, AuthenticationWorkflowResult>( | |
ident, | |
authe, | |
autho, | |
normalize | |
); | |
return f(toAuthenticationVector(login, password, remindMe)); | |
}; | |
}; | |
} | |
const apiScopes = ["resources:write"]; | |
const login = "root"; | |
const password = "demodemo"; | |
const remindUser = Iam.REMIND_USER; | |
const authenticateUser = Iam.getAuthenticationWorkflow( | |
{ | |
identify: Iam.identify, | |
authenticate: Iam.authenticate, | |
authorize: Iam.authorize, | |
}, | |
apiScopes | |
); | |
const r = authenticateUser(login, password, remindUser); | |
console.log(r); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment