diff --git a/packages/base/LICENSE b/packages/base/LICENSE new file mode 100644 index 0000000..6868a4d --- /dev/null +++ b/packages/base/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 @auth-tools + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/base/local/index.ts b/packages/base/local/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/base/package.json b/packages/base/package.json new file mode 100644 index 0000000..6ffff23 --- /dev/null +++ b/packages/base/package.json @@ -0,0 +1,29 @@ +{ + "name": "@auth-tools/base", + "version": "0.0.1-alpha.2", + "description": "A structured authentication protocol for Javascript. (base)", + "main": "dist/index.js", + "repository": "https://github.com/auth-tools/auth-tools", + "author": "Laurenz Rausche ", + "license": "MIT", + "private": false, + "scripts": { + "build": "npm run build:remove && tsc", + "build:remove": "rimraf dist", + "local": "npm run build && ts-node-dev --respawn local/index.ts", + "prepublish": "npm run build", + "remove": "npm run build:remove && rimraf node_modules yarn.lock package-lock.json pnpm-lock.yaml" + }, + "dependencies": {}, + "devDependencies": { + "@auth-tools/base": "link:.", + "@auth-tools/logger": "^0.0.1-alpha.1", + "rimraf": "^5.0.5", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5" + }, + "peerDependencies": {}, + "files": [ + "dist" + ] +} diff --git a/packages/base/src/auth.ts b/packages/base/src/auth.ts new file mode 100644 index 0000000..372ae4c --- /dev/null +++ b/packages/base/src/auth.ts @@ -0,0 +1,73 @@ +import { + InterceptEventCallback, + InterceptEventCallbacks, + InterceptEvents, + InterceptEventsDefinition, +} from "./events/intercept"; +import { + UseEventCallback, + UseEventCallbacks, + UseEvents, + UseEventsDefinition, +} from "./events/use"; +import { DeepRequired } from "./helpers"; +import { LogFunction } from "@auth-tools/logger"; + +//definition of internal data of AuthBase class +export type AuthInternal< + AuthConfig, + ClassUseEvents extends UseEventsDefinition, + ClassInterceptEvents extends InterceptEventsDefinition +> = { + config: DeepRequired; + log: LogFunction; + useEventCallbacks: UseEventCallbacks; + interceptEventCallbacks: InterceptEventCallbacks; +}; + +//auth base class +export class AuthBase< + AuthConfig, + ClassUseEvents extends UseEventsDefinition, + ClassInterceptEvents extends InterceptEventsDefinition +> { + //internal auth data + public _internal: AuthInternal< + AuthConfig, + ClassUseEvents, + ClassInterceptEvents + >; + + constructor( + config: DeepRequired, + log: LogFunction, + defaultUseEvents: UseEventCallbacks, + defaultInterceptEvents: InterceptEventCallbacks + ) { + //sets _internal + this._internal = { + config: config, + log: log, + useEventCallbacks: defaultUseEvents, + interceptEventCallbacks: defaultInterceptEvents, + }; + } + + //sets a use event + public use>( + event: UseEventName, + callback: UseEventCallback + ): void { + this._internal.useEventCallbacks[event] = callback; + } + + //sets a intercept event + public intercept< + InterceptEventName extends keyof InterceptEvents + >( + event: InterceptEventName, + callback: InterceptEventCallback + ): void { + this._internal.interceptEventCallbacks[event] = callback; + } +} diff --git a/packages/base/src/events/intercept.ts b/packages/base/src/events/intercept.ts new file mode 100644 index 0000000..d7606a3 --- /dev/null +++ b/packages/base/src/events/intercept.ts @@ -0,0 +1,50 @@ +import { Promisify } from "../index"; + +//definition for a intercept event (only used for auto completion with extends) +type InterceptEventDefinition = { + data: Data; +}; + +//definition for intercept events (only used for auto completion with extends) +export type InterceptEventsDefinition = { + [InterceptEventName: string]: InterceptEventDefinition; +}; + +//data for an intercept event +type InterceptEvent = { + data: ClassInterceptEvents["data"]; + return: { + serverError?: boolean; + intercepted: boolean; + interceptCode: number; + }; +}; + +//all intercept events +export type InterceptEvents< + ClassInterceptEvents extends InterceptEventsDefinition +> = { + [InterceptEventName in keyof ClassInterceptEvents]: InterceptEvent< + ClassInterceptEvents[InterceptEventName] + >; +}; + +//constructed callback for intercept event +export type InterceptEventCallback< + ClassInterceptEvents extends InterceptEventsDefinition, + InterceptEventName extends keyof InterceptEvents +> = ( + data: InterceptEvents[InterceptEventName]["data"] +) => Promisify< + InterceptEvents[InterceptEventName]["return"] +>; + +//all callbacks for intercept events +export type InterceptEventCallbacks< + ClassInterceptEvents extends InterceptEventsDefinition +> = { + [InterceptEventName in keyof InterceptEvents]: InterceptEventCallback< + ClassInterceptEvents, + InterceptEventName + >; +}; diff --git a/packages/base/src/events/use.ts b/packages/base/src/events/use.ts new file mode 100644 index 0000000..5831a36 --- /dev/null +++ b/packages/base/src/events/use.ts @@ -0,0 +1,41 @@ +import { Promisify } from "../index"; + +//definition for a use event (only used for auto completion with extends) +type UseEventDefinition = { + data: Data; + return: Return; +}; + +//definition for use events (only used for auto completion with extends) +export type UseEventsDefinition = { + [UseEventName: string]: UseEventDefinition; +}; + +//data for an use event +type UseEvent = { + data: ClassUseEvents["data"]; + return: { serverError?: boolean } & ClassUseEvents["return"]; +}; + +//all use events +export type UseEvents = { + [UseEventName in keyof ClassUseEvents]: UseEvent< + ClassUseEvents[UseEventName] + >; +}; + +//constructed callback for use event +export type UseEventCallback< + ClassUseEvents extends UseEventsDefinition, + UseEventName extends keyof UseEvents +> = ( + data: UseEvents[UseEventName]["data"] +) => Promisify[UseEventName]["return"]>; + +//all callbacks for use events +export type UseEventCallbacks = { + [UseEventName in keyof UseEvents]: UseEventCallback< + ClassUseEvents, + UseEventName + >; +}; diff --git a/packages/base/src/helpers.ts b/packages/base/src/helpers.ts new file mode 100644 index 0000000..cad8225 --- /dev/null +++ b/packages/base/src/helpers.ts @@ -0,0 +1,12 @@ +import { User } from "./protocol"; + +export type Promisify = Promise | Type; +export type DeepRequired = { [K in keyof T]-?: DeepRequired }; +export type DeepOptional = { [K in keyof T]?: DeepRequired }; +export type KeysStartingWith = { + [Key in keyof Type as Key extends `${Str}_${infer _}` + ? Key + : never]: Type[Key]; +}; +export type UserData = User<"id" | "email" | "username" | "hashedPassword">; +export type Payload = User<"id">; diff --git a/packages/base/src/index.ts b/packages/base/src/index.ts new file mode 100644 index 0000000..f67885c --- /dev/null +++ b/packages/base/src/index.ts @@ -0,0 +1,13 @@ +export { AuthBase } from "./auth"; +export type { AuthInternal } from "./auth"; +export type { InterceptEventCallbacks } from "./events/intercept"; +export type { UseEventCallbacks } from "./events/use"; +export type { + DeepOptional, + DeepRequired, + Payload, + Promisify, + UserData, +} from "./helpers"; +export type { AuthProtocol, AuthRequest, AuthResponse, User } from "./protocol"; +export type { AuthMessages } from "./protocol/messages"; diff --git a/packages/base/src/protocol/index.ts b/packages/base/src/protocol/index.ts new file mode 100644 index 0000000..5ec6530 --- /dev/null +++ b/packages/base/src/protocol/index.ts @@ -0,0 +1,169 @@ +import { DeepOptional, KeysStartingWith } from "../helpers"; +import { AuthMessages } from "./messages"; + +//all possible method names +type AuthMethodStrings = + | "server" + | "validate" + | "register" + | "login" + | "logout" + | "refresh" + | "check"; + +//type builder for an auth response +type AuthResponseBuilder< + Method extends AuthMethodStrings, + StatusCode extends number, + ResponseData extends any, + InterceptCode extends number = 0, + AuthMessage = AuthMessages[`${Method}_${StatusCode}`] +> = { + auth: { + error: StatusCode extends 0 ? false : true; + errorType: Method extends "server" ? "server" : "method"; + message: AuthMessage; + codes: { + status: StatusCode; + intercept: InterceptCode; + }; + }; + data: ResponseData; +}; + +//definition for argument for an auth response builder +type AuthMethodResponsesDefinition = { + [ResponseName in keyof KeysStartingWith< + AuthMessages, + Method + >]: AuthResponseBuilder; +}; + +//type that builds all responses in one object +type AuthMethodResponses< + Method extends AuthMethodStrings, + Responses extends AuthMethodResponsesDefinition +> = { + [ResponseName in keyof Responses]: Responses[ResponseName]; +} & { + server_5: AuthResponseBuilder<"server", 5, null>; +}; + +//type that builds an entire method +type AuthMethodBuilder< + Method extends AuthMethodStrings, + Request extends any, + Responses extends AuthMethodResponsesDefinition +> = { + request: DeepOptional; + responses: AuthMethodResponses; +}; + +//all data of a user +type UserData = { + id: string; + login: string; + email: string; + username: string; + password: string; + hashedPassword: string; + accessToken: string; + refreshToken: string; +}; + +//a user map +export type User = Pick; + +//all auth methods +export type AuthProtocol = { + validate: AuthMethodBuilder< + "validate", + User<"accessToken">, + { + validate_0: AuthResponseBuilder<"validate", 0, User<"id">>; + validate_1: AuthResponseBuilder<"validate", 1, null>; + validate_2: AuthResponseBuilder<"validate", 2, null>; + validate_3: AuthResponseBuilder<"validate", 3, null>; + validate_9: AuthResponseBuilder<"validate", 9, null, number>; + } + >; + register: AuthMethodBuilder< + "register", + User<"email" | "username" | "password">, + { + register_0: AuthResponseBuilder< + "register", + 0, + User<"id" | "email" | "username"> + >; + register_1: AuthResponseBuilder<"register", 1, null>; + register_2: AuthResponseBuilder<"register", 2, null>; + register_3: AuthResponseBuilder<"register", 3, null>; + register_4: AuthResponseBuilder<"register", 4, null>; + register_5: AuthResponseBuilder<"register", 5, null>; + register_6: AuthResponseBuilder<"register", 6, null>; + register_7: AuthResponseBuilder<"register", 7, null>; + register_9: AuthResponseBuilder<"register", 9, null, number>; + } + >; + login: AuthMethodBuilder< + "login", + User<"login" | "password">, + { + login_0: AuthResponseBuilder< + "login", + 0, + User<"accessToken" | "refreshToken"> + >; + login_1: AuthResponseBuilder<"login", 1, null>; + login_2: AuthResponseBuilder<"login", 2, null>; + login_3: AuthResponseBuilder<"login", 3, null>; + login_4: AuthResponseBuilder<"login", 4, null>; + login_5: AuthResponseBuilder<"login", 5, null>; + login_9: AuthResponseBuilder<"login", 9, null, number>; + } + >; + logout: AuthMethodBuilder< + "logout", + User<"refreshToken">, + { + logout_0: AuthResponseBuilder<"logout", 0, null>; + logout_1: AuthResponseBuilder<"logout", 1, null>; + logout_2: AuthResponseBuilder<"logout", 2, null>; + logout_3: AuthResponseBuilder<"logout", 3, null>; + logout_4: AuthResponseBuilder<"logout", 4, null>; + logout_9: AuthResponseBuilder<"logout", 9, null, number>; + } + >; + refresh: AuthMethodBuilder< + "refresh", + User<"refreshToken">, + { + refresh_0: AuthResponseBuilder<"refresh", 0, User<"accessToken">>; + refresh_1: AuthResponseBuilder<"refresh", 1, null>; + refresh_2: AuthResponseBuilder<"refresh", 2, null>; + refresh_3: AuthResponseBuilder<"refresh", 3, null>; + refresh_4: AuthResponseBuilder<"refresh", 4, null>; + refresh_9: AuthResponseBuilder<"refresh", 9, null, number>; + } + >; + + check: AuthMethodBuilder< + "check", + User<"accessToken" | "refreshToken">, + { + check_0: AuthResponseBuilder<"check", 0, null>; + check_1: AuthResponseBuilder<"check", 1, null>; + check_2: AuthResponseBuilder<"check", 2, null>; + check_3: AuthResponseBuilder<"check", 3, null>; + check_4: AuthResponseBuilder<"check", 4, null>; + check_5: AuthResponseBuilder<"check", 5, null>; + check_9: AuthResponseBuilder<"check", 9, null, number>; + } + >; +}; + +export type AuthRequest> = + AuthProtocol[Method]["request"]; +export type AuthResponse> = + AuthProtocol[Method]["responses"][keyof AuthProtocol[Method]["responses"]]; diff --git a/packages/base/src/protocol/messages.ts b/packages/base/src/protocol/messages.ts new file mode 100644 index 0000000..4b07d01 --- /dev/null +++ b/packages/base/src/protocol/messages.ts @@ -0,0 +1,44 @@ +export type AuthMessages = { + [Key: `${string}_${number}`]: string; + server_5: "An error occurred on the server. Please try again later."; + validate_0: "Validation successful."; + validate_1: "The validation method is disabled."; + validate_2: 'The "accessToken" is missing.'; + validate_3: 'The "accessToken" is invalid.'; + validate_9: "The validation request was intercepted."; + register_0: "Registration successful."; + register_1: "The registration method is disabled."; + register_2: 'The "email", "username" or "password" is missing.'; + register_3: 'The "email" is malformed.'; + register_4: 'The "password" is too weak.'; + register_5: 'The "email" is already in use.'; + register_6: 'The "username" is already in use.'; + register_7: 'The "login" is already in use.'; + register_9: "The registration request was intercepted."; + login_0: "Login successful."; + login_1: "The login method is disabled."; + login_2: 'The "login" ("email" or "username") or "password" is missing.'; + login_3: "The user was not found."; + login_4: 'The "password" is incorrect.'; + login_5: 'The user was not found or the "password" is incorrect.'; + login_9: "The login request was intercepted."; + logout_0: "Logout successful."; + logout_1: "The logout method is disabled."; + logout_2: 'The "refreshToken" is missing.'; + logout_3: 'The "refreshToken" is invalid.'; + logout_4: 'The "refreshToken" does not exist.'; + logout_9: "The logout request was intercepted."; + refresh_0: "Refresh successful."; + refresh_1: "The refresh method is disabled."; + refresh_2: 'The "refreshToken" is missing.'; + refresh_3: 'The "refreshToken" is invalid.'; + refresh_4: 'The "refreshToken" does not exist.'; + refresh_9: "The refresh request was intercepted."; + check_0: "Check successful."; + check_1: "The check method is disabled."; + check_2: 'The "accessToken" or "refreshToken" is missing.'; + check_3: 'The "refreshToken" is invalid.'; + check_4: 'The "refreshToken" does not exist.'; + check_5: 'The "accessToken" is invalid.'; + check_9: "The check request was intercepted."; +}; diff --git a/packages/base/tsconfig.json b/packages/base/tsconfig.json new file mode 100644 index 0000000..ce1e038 --- /dev/null +++ b/packages/base/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2016", + "lib": ["ES2016"], + "module": "CommonJS", + "moduleResolution": "Node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/client/LICENSE b/packages/client/LICENSE new file mode 100644 index 0000000..6868a4d --- /dev/null +++ b/packages/client/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 @auth-tools + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/client/local/index.ts b/packages/client/local/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 0000000..703c07b --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,28 @@ +{ + "name": "@auth-tools/client", + "version": "0.0.0", + "description": "A structured authentication protocol for Javascript. (client)", + "main": "dist/index.js", + "repository": "https://github.com/auth-tools/auth-tools", + "author": "Laurenz Rausche ", + "license": "MIT", + "private": false, + "scripts": { + "build": "npm run build:remove && tsc", + "build:remove": "rimraf dist", + "local": "npm run build && ts-node-dev --respawn local/index.ts", + "prepublish": "npm run build", + "remove": "npm run build:remove && rimraf node_modules yarn.lock package-lock.json pnpm-lock.yaml" + }, + "dependencies": {}, + "devDependencies": { + "@auth-tools/client": "link:.", + "rimraf": "^5.0.5", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5" + }, + "peerDependencies": {}, + "files": [ + "dist" + ] +} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json new file mode 100644 index 0000000..ce1e038 --- /dev/null +++ b/packages/client/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2016", + "lib": ["ES2016"], + "module": "CommonJS", + "moduleResolution": "Node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/logger/LICENSE b/packages/logger/LICENSE new file mode 100644 index 0000000..6868a4d --- /dev/null +++ b/packages/logger/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 @auth-tools + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/logger/local/index.ts b/packages/logger/local/index.ts new file mode 100644 index 0000000..a841710 --- /dev/null +++ b/packages/logger/local/index.ts @@ -0,0 +1,66 @@ +import DefaultLogger, { COLORS, Logger } from "@auth-tools/logger"; + +DefaultLogger.log("debug", `"debug" log with DefaultLogger`); +DefaultLogger.log("info", `"info" log with DefaultLogger`); +DefaultLogger.log("warn", `"warn" log with DefaultLogger`); +DefaultLogger.log("error", `"error" log with DefaultLogger`); + +//set config after creation +DefaultLogger.setConfig({ + disableColor: true, + formatString: "Color disabled: [%l] %d %m", +}); + +//log shorthands +DefaultLogger.debug(`"debug" log with DefaultLogger and updated config`); +DefaultLogger.info(`"info" log with DefaultLogger and updated config`); +DefaultLogger.warn(`"warn" log with DefaultLogger and updated config`); +DefaultLogger.error(`"error" log with DefaultLogger and updated config`); + +//multiple arguments +DefaultLogger.log("error", `Argument 1`, "Argument 2"); + +//update config again +DefaultLogger.setConfig({ disableColor: false, formatString: "Built-In: %m" }); + +//builtin Methods +DefaultLogger.log("debug", DefaultLogger.color("Red", COLORS.error)); +DefaultLogger.log("info", DefaultLogger.twoDigits("1")); +DefaultLogger.log("warn", DefaultLogger.currentTime()); +DefaultLogger.log("error", DefaultLogger.currentDate()); + +//create custom logger instance +const logger2 = new Logger({ + disableColor: false, //set to true to disable all colors + formatString: [ + //all replacement vars: + "Vars: %t = HH:MM:SS", + "Vars: %d = YYYY-MM-DD", + "Vars: %L = LEVEL", + "Vars: %l = level", + "Vars: %m = message", + ].join("\n"), +}); + +logger2.log("error", "Hallo"); + +//update config to disable debug and warn +logger2.setConfig({ + formatString: "Disabled Methods: [%L] %t %m", + methods: { + debug: false, + warn: false, + }, +}); + +logger2.debug("Shouldn't log"); +logger2.info("Should log"); +logger2.warn("Shouldn't log"); +logger2.error("Should log"); + +//format string with current instance config +logger2.setConfig({ + formatString: "Format String: [%m]", +}); +const formatted = logger2.format("info", "MY CUSTOM STRING"); +console.log(formatted); diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..35757a4 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,29 @@ +{ + "name": "@auth-tools/logger", + "version": "0.0.1-alpha.1", + "description": "A structured authentication protocol for Javascript. (logger)", + "main": "dist/index.js", + "repository": "https://github.com/auth-tools/auth-tools", + "author": "Laurenz Rausche ", + "license": "MIT", + "private": false, + "scripts": { + "build": "npm run build:remove && tsc", + "build:remove": "rimraf dist", + "local": "npm run build && ts-node-dev --respawn local/index.ts", + "prepublish": "npm run build", + "remove": "npm run build:remove && rimraf node_modules yarn.lock package-lock.json pnpm-lock.yaml" + }, + "dependencies": {}, + "devDependencies": { + "@auth-tools/logger": "link:.", + "@types/node": "^20.12.7", + "rimraf": "^5.0.5", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5" + }, + "peerDependencies": {}, + "files": [ + "dist" + ] +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000..0b975a7 --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,7 @@ +import { Logger } from "./logger"; + +//create default logger instance +const DefaultLogger = new Logger(); + +export * from "./logger"; +export default DefaultLogger; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts new file mode 100644 index 0000000..16a0e56 --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,148 @@ +//util types +type DeepRequired = { [K in keyof T]-?: DeepRequired }; + +//types for the logger +export type LogLevels = "debug" | "info" | "warn" | "error"; +export type LogFunction = (level: LogLevels, ...message: string[]) => void; +export type LogConfig = { + disableColor?: boolean; + formatString?: string; + methods?: { + [key in LogLevels]?: boolean; + }; +}; + +//enum for all ansi colors +export enum COLORS { + time = "90;1", + date = "32;1", + debug = "35;1", + info = "36", + warn = "33", + error = "31;1", +} + +export class Logger { + private config: DeepRequired; + constructor(config?: LogConfig) { + //make sure log functions contexts are set to class + this.debug = this.debug.bind(this); + this.info = this.info.bind(this); + this.warn = this.warn.bind(this); + this.error = this.error.bind(this); + this.setConfig = this.setConfig.bind(this); + this.log = this.log.bind(this); + this.color = this.color.bind(this); + this.format = this.format.bind(this); + this.twoDigits = this.twoDigits.bind(this); + this.currentTime = this.currentTime.bind(this); + this.currentDate = this.currentDate.bind(this); + + //set config with defaults + this.config = { + disableColor: config?.disableColor ?? false, + formatString: config?.formatString ?? "[%L] %t %m", + methods: { + debug: config?.methods?.debug ?? true, + info: config?.methods?.info ?? true, + warn: config?.methods?.warn ?? true, + error: config?.methods?.error ?? true, + }, + }; + } + + //shorthand for Logger.log("debug", ...) + public debug(...messages: string[]): void { + this.log("debug", ...messages); + } + + //shorthand for Logger.log("info", ...) + public info(...messages: string[]): void { + this.log("info", ...messages); + } + + //shorthand for Logger.log("warn", ...) + public warn(...messages: string[]): void { + this.log("warn", ...messages); + } + + //shorthand for Logger.log("error", ...) + public error(...messages: string[]): void { + this.log("error", ...messages); + } + + //config after initial creation + public setConfig(config?: LogConfig): void { + this.config = { + disableColor: config?.disableColor ?? this.config.disableColor, + formatString: config?.formatString ?? this.config.formatString, + methods: { + debug: config?.methods?.debug ?? this.config.methods.debug, + info: config?.methods?.info ?? this.config.methods.info, + warn: config?.methods?.warn ?? this.config.methods.warn, + error: config?.methods?.error ?? this.config.methods.error, + }, + }; + } + + //log the message + public log(level: LogLevels, ...messages: string[]): void { + messages.forEach((message) => { + if (this.config.methods[level]) + console[level](this.format(level, message)); + }); + } + + //color a string + public color(str: string, ansiColorValue: string): string { + return this.config.disableColor + ? str + : `\x1b[${ansiColorValue}m${str}\x1b[0m`; + } + + //format multi line logs + public format(level: LogLevels, message: string): string { + return message + .split("\n") + .map((line) => this.formatLine(level, line)) + .join("\n"); + } + + public twoDigits(str: string): string { + return str.length === 1 ? `0${str}` : str; + } + + //format current time as HH:MM:SS + public currentTime(): string { + const date = new Date(); + return ( + this.twoDigits(date.getHours().toString()) + + ":" + + this.twoDigits(date.getMinutes().toString()) + + ":" + + this.twoDigits(date.getSeconds().toString()) + ); + } + + //format current date as YYYY-MM-DD + public currentDate(): string { + const date = new Date(); + return ( + this.twoDigits(date.getFullYear().toString()) + + "-" + + this.twoDigits(date.getMonth().toString()) + + "-" + + this.twoDigits(date.getDate().toString()) + ); + } + + //format a single log line + private formatLine(level: LogLevels, line: string): string { + return this.config.formatString + .replace("%t", this.color(this.currentTime(), COLORS["time"])) //replace %t with current date (HH:MM:SS) + .replace("%d", this.color(this.currentDate(), COLORS["date"])) //replace %d with current date (YYYY-MM-DD) + .replace("%L", this.color(level.toUpperCase(), COLORS[level])) //replace %L (uppercase level) with given level + .replace("%l", this.color(level.toLowerCase(), COLORS[level])) //replace %l (lowercase level) with given level + .replace("%m", line); //replace %m (message) with given message + } +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000..ce1e038 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2016", + "lib": ["ES2016"], + "module": "CommonJS", + "moduleResolution": "Node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/server/LICENSE b/packages/server/LICENSE new file mode 100644 index 0000000..6868a4d --- /dev/null +++ b/packages/server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 @auth-tools + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/server/local/index.ts b/packages/server/local/index.ts new file mode 100644 index 0000000..f1bc454 --- /dev/null +++ b/packages/server/local/index.ts @@ -0,0 +1,72 @@ +import { UserData } from "@auth-tools/base"; +import { Logger } from "@auth-tools/logger"; +import { AuthServer, AuthServerConfig } from "@auth-tools/server"; + +const Users: UserData[] = []; +const Tokens: string[] = []; + +const logger = new Logger(); + +const authServerConfig: AuthServerConfig = { + secrets: { + accessToken: "SECRET", + refreshToken: "SECRET", + }, +}; + +const authServer = new AuthServer(authServerConfig, logger.log); + +authServer.use("getUserByMail", ({ email }) => { + const user = Users.find((usr) => usr.email === email) || null; + return { serverError: false, user }; +}); + +authServer.use("getUserByName", ({ username }) => { + const user = Users.find((usr) => usr.username === username) || null; + return { serverError: false, user }; +}); + +authServer.use("storeUser", ({ user }) => { + Users.push(user); + return { serverError: false }; +}); + +authServer.use("checkToken", ({ refreshToken }) => { + const exists = Tokens.includes(refreshToken); + return { serverError: false, exists }; +}); + +authServer.use("storeToken", ({ refreshToken }) => { + Tokens.push(refreshToken); + return { serverError: false }; +}); + +authServer.use("deleteToken", ({ refreshToken }) => { + Tokens.splice(Tokens.indexOf(refreshToken), 1); + return { serverError: false }; +}); + +authServer.use("validateMail", ({ email }) => { + const isValid = email.includes("@"); + return { serverError: false, isValid }; +}); + +authServer.use("validatePassword", ({ password }) => { + const isValid = password.length >= 8; + return { serverError: false, isValid }; +}); + +authServer.use("hashPassword", ({ password }) => { + const hashedPassword = password.split("").reverse().join(""); + return { serverError: false, hashedPassword }; +}); + +authServer.use("checkPassword", ({ password, hashedPassword }) => { + const matches = password.split("").reverse().join("") === hashedPassword; + return { serverError: false, matches }; +}); + +authServer.use("genId", ({}) => { + const id = Users.length.toString(); + return { serverError: false, id }; +}); diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..2261f16 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,33 @@ +{ + "name": "@auth-tools/server", + "version": "0.0.1-alpha.1", + "description": "A structured authentication protocol for Javascript. (server)", + "main": "dist/index.js", + "repository": "https://github.com/auth-tools/auth-tools", + "author": "Laurenz Rausche ", + "license": "MIT", + "private": false, + "scripts": { + "build": "npm run build:remove && tsc", + "build:remove": "rimraf dist", + "local": "npm run build && ts-node-dev --respawn local/index.ts", + "prepublish": "npm run build", + "remove": "npm run build:remove && rimraf node_modules yarn.lock package-lock.json pnpm-lock.yaml" + }, + "dependencies": { + "@auth-tools/base": "^0.0.1-alpha.2", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "@auth-tools/logger": "^0.0.1-alpha.1", + "@auth-tools/server": "link:.", + "@types/jsonwebtoken": "^9.0.6", + "rimraf": "^5.0.5", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5" + }, + "peerDependencies": {}, + "files": [ + "dist" + ] +} diff --git a/packages/server/src/auth.ts b/packages/server/src/auth.ts new file mode 100644 index 0000000..17a0b85 --- /dev/null +++ b/packages/server/src/auth.ts @@ -0,0 +1,227 @@ +import { + AuthBase, + AuthProtocol, + AuthRequest, + AuthResponse, + DeepRequired, + InterceptEventCallbacks, + UseEventCallbacks, + User, +} from "@auth-tools/base"; +import { LogFunction } from "@auth-tools/logger"; +import { undefinedInterceptEvent, undefinedUseEvent } from "./events"; +import { createRegister } from "./methods/register"; +import { createLogin } from "./methods/login"; +import { createLogout } from "./methods/logout"; +import { createRefresh } from "./methods/refresh"; +import { createCheck } from "./methods/check"; +import { createValidate } from "./methods/validate"; + +//states of a method +type MethodState = "active" | "disabled" | "removed"; + +//config passed by user to class +export type AuthServerConfig = { + secrets: { + accessToken: string; + refreshToken: string; + }; + expiresIn?: number; + sensitive?: { + api?: boolean; + logs?: boolean; + }; + methods?: { + validate?: MethodState; + register?: MethodState; + login?: MethodState; + logout?: MethodState; + refresh?: MethodState; + check?: MethodState; + }; +}; + +//all use events +export type AuthServerUseEvents = { + getUserByMail: { + data: User<"email">; + return: { + user: User<"id" | "email" | "username" | "hashedPassword"> | null; + }; + }; + getUserByName: { + data: User<"username">; + return: { + user: User<"id" | "email" | "username" | "hashedPassword"> | null; + }; + }; + hashPassword: { + data: User<"password">; + return: User<"hashedPassword">; + }; + checkToken: { + data: User<"refreshToken">; + return: { exists: boolean }; + }; + storeToken: { + data: User<"refreshToken">; + return: {}; + }; + deleteToken: { + data: User<"refreshToken">; + return: {}; + }; + validateMail: { + data: User<"email">; + return: { isValid: boolean }; + }; + validatePassword: { + data: User<"password">; + return: { isValid: boolean }; + }; + genId: { + data: User<"email" | "username">; + return: { id: string }; + }; + storeUser: { + data: { user: User<"id" | "email" | "username" | "hashedPassword"> }; + return: {}; + }; + checkPassword: { + data: User<"password" | "hashedPassword">; + return: { matches: boolean }; + }; +}; + +//all intercept events +export type AuthServerInterceptEvents = { + register: { + data: { user: User<"id" | "email" | "username" | "hashedPassword"> }; + }; + login: { + data: User<"accessToken" | "refreshToken"> & { + user: User<"id" | "email" | "username" | "hashedPassword">; + payload: User<"id">; + }; + }; + logout: { + data: User<"refreshToken"> & { payload: User<"id"> }; + }; + refresh: { + data: User<"refreshToken"> & { payload: User<"id"> }; + }; + check: { + data: User<"accessToken" | "refreshToken"> & { + payload: User<"id">; + }; + }; +}; + +export type AuthServerMethod = ( + data: AuthRequest +) => Promise>; + +type AuthServerMethods = { + [MethodName in keyof AuthProtocol]?: AuthServerMethod; +}; + +export class AuthServer extends AuthBase< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents +> { + //all methods + public methods: AuthServerMethods; + + //auth server constructor + constructor(config: AuthServerConfig, log: LogFunction) { + //config with default values + const defaultedConfig: DeepRequired = { + secrets: { + accessToken: config.secrets.accessToken, + refreshToken: config.secrets.refreshToken, + }, + expiresIn: config.expiresIn ?? 900, //by default accessToken expires in 900s (15min) + sensitive: { + api: config.sensitive?.api ?? true, //by default api will not directly expose type of error which could leak information of the database + logs: config.sensitive?.logs ?? false, //by default logs WILL directly expose type of error which could leak information of the database + }, + methods: { + validate: config.methods?.validate ?? "active", //by default validate is active + register: config.methods?.register ?? "active", //by default register is active + login: config.methods?.login ?? "active", //by default login is active + logout: config.methods?.logout ?? "active", //by default logout is active + refresh: config.methods?.refresh ?? "active", //by default refresh is active + check: config.methods?.check ?? "active", //by default check is active + }, + }; + + //defaults for use event callbacks + const defaultedUseEvents: UseEventCallbacks = { + getUserByMail: undefinedUseEvent("getUserByMail", { user: null }, log), + getUserByName: undefinedUseEvent("getUserByName", { user: null }, log), + storeUser: undefinedUseEvent("storeUser", {}, log), + checkToken: undefinedUseEvent("checkToken", { exists: false }, log), + storeToken: undefinedUseEvent("storeToken", {}, log), + deleteToken: undefinedUseEvent("deleteToken", {}, log), + validateMail: undefinedUseEvent("validateMail", { isValid: false }, log), + validatePassword: undefinedUseEvent( + "validatePassword", + { isValid: false }, + log + ), + hashPassword: undefinedUseEvent( + "hashPassword", + { hashedPassword: "" }, + log + ), + genId: undefinedUseEvent("genId", { id: "" }, log), + checkPassword: undefinedUseEvent( + "checkPassword", + { matches: false }, + log + ), + }; + + //defaults for intercept event callbacks + const defaultedInterceptEvents: InterceptEventCallbacks = + { + register: undefinedInterceptEvent<"register">(), + login: undefinedInterceptEvent<"login">(), + logout: undefinedInterceptEvent<"logout">(), + refresh: undefinedInterceptEvent<"refresh">(), + check: undefinedInterceptEvent<"check">(), + }; + + //init authbase class + super(defaultedConfig, log, defaultedUseEvents, defaultedInterceptEvents); + + //all auth methods + this.methods = { + validate: + this._internal.config.methods.validate !== "removed" + ? createValidate(this._internal) + : undefined, + register: + this._internal.config.methods.register !== "removed" + ? createRegister(this._internal) + : undefined, + login: + this._internal.config.methods.login !== "removed" + ? createLogin(this._internal) + : undefined, + logout: + this._internal.config.methods.logout !== "removed" + ? createLogout(this._internal) + : undefined, + refresh: + this._internal.config.methods.refresh !== "removed" + ? createRefresh(this._internal) + : undefined, + check: + this._internal.config.methods.check !== "removed" + ? createCheck(this._internal) + : undefined, + }; + } +} diff --git a/packages/server/src/events.ts b/packages/server/src/events.ts new file mode 100644 index 0000000..3c991af --- /dev/null +++ b/packages/server/src/events.ts @@ -0,0 +1,28 @@ +import { InterceptEventCallbacks, UseEventCallbacks } from "@auth-tools/base"; +import { AuthServerInterceptEvents, AuthServerUseEvents } from "./auth"; +import { LogFunction } from "@auth-tools/logger"; + +//for an undefined use event +export function undefinedUseEvent< + Event extends keyof AuthServerUseEvents, + Return extends AuthServerUseEvents[Event]["return"] +>( + event: Event, + returnData: Return, + log: LogFunction +): UseEventCallbacks[Event] { + return (() => { + //complain about unset use event callback + log("error", `The use "${event}" event is not defined!`); + return { ...returnData, serverError: true }; + }) as UseEventCallbacks[Event]; +} + +//for an undefined intercept event +export function undefinedInterceptEvent< + Event extends keyof AuthServerInterceptEvents +>(): InterceptEventCallbacks[Event] { + return () => { + return { serverError: false, intercepted: false, interceptCode: 0 }; + }; +} diff --git a/packages/server/src/getUserByLogin.ts b/packages/server/src/getUserByLogin.ts new file mode 100644 index 0000000..2c9c891 --- /dev/null +++ b/packages/server/src/getUserByLogin.ts @@ -0,0 +1,34 @@ +import { AuthInternal, UseEventCallbacks, User } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerUseEvents, +} from "./auth"; + +export default async function ( + login: User<"login">["login"], + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): Promise< + | ReturnType["getUserByMail"]> + | ReturnType["getUserByName"]> +> { + //get user by email with value of login + const getUserByMail = await internal.useEventCallbacks.getUserByMail({ + email: login, + }); + + if (getUserByMail.serverError) return { serverError: true, user: null }; + + //get user by name with value of login + const getUserByName = await internal.useEventCallbacks.getUserByName({ + username: login, + }); + + if (getUserByName.serverError) return { serverError: true, user: null }; + + return { serverError: false, user: getUserByMail.user || getUserByName.user }; +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..41fea60 --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,3 @@ +export { AuthServer as default } from "./auth"; +export { AuthServer, AuthServerConfig, AuthServerMethod } from "./auth"; +export { TokenPayload } from "./tokenUtils"; \ No newline at end of file diff --git a/packages/server/src/methods/_template.ts.txt b/packages/server/src/methods/_template.ts.txt new file mode 100644 index 0000000..95540ff --- /dev/null +++ b/packages/server/src/methods/_template.ts.txt @@ -0,0 +1,31 @@ +import { AuthInternal } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerMethod, + AuthServerUseEvents, +} from "../auth"; +import { authError, authServerError } from "../senders"; + +//create _method method +export function create_Method( + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): AuthServerMethod<"_method"> { + return async ({}) => { + //_method method is disabled + if (internal.config.methods._method === "disabled") { + internal.log("debug", "The _method method is disabled."); + return authError<"_method", 1>(1, "The _method method is disabled."); + } + + try { + } catch (error) { + internal.log("warn", String(error)); + return authServerError(); + } + }; +} diff --git a/packages/server/src/methods/check.ts b/packages/server/src/methods/check.ts new file mode 100644 index 0000000..4cf6e78 --- /dev/null +++ b/packages/server/src/methods/check.ts @@ -0,0 +1,98 @@ +import { AuthInternal } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerMethod, + AuthServerUseEvents, +} from "../auth"; +import { authError, authServerError } from "../senders"; +import { decodeToken } from "../tokenUtils"; + +//create check method +export function createCheck( + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): AuthServerMethod<"check"> { + return async ({ accessToken, refreshToken }) => { + //check method is disabled + if (internal.config.methods.check === "disabled") { + internal.log("debug", "The check method is disabled."); + return authError<"check", 1>(1, "The check method is disabled."); + } + + try { + if (!accessToken || !refreshToken) { + internal.log( + "debug", + 'The "accessToken" or "refreshToken" is missing.' + ); + return authError<"check", 2>( + 2, + 'The "accessToken" or "refreshToken" is missing.' + ); + } + + const decodeRefreshToken = decodeToken( + refreshToken, + internal.config.secrets.refreshToken + ); + + if (!decodeRefreshToken.valid || !decodeRefreshToken.payload) { + internal.log("debug", 'The "refreshToken" is invalid.'); + return authError<"check", 3>(3, 'The "refreshToken" is invalid.'); + } + + const checkToken = await internal.useEventCallbacks.checkToken({ + refreshToken, + }); + + if (checkToken.serverError) return authServerError(); + + if (!checkToken.exists) { + internal.log("debug", 'The "refreshToken" does not exist.'); + return authError<"check", 4>(4, 'The "refreshToken" does not exist.'); + } + + const decodeAccessToken = decodeToken( + refreshToken, + internal.config.secrets.accessToken + ); + + if (!decodeAccessToken.valid || !decodeAccessToken.payload) { + internal.log("debug", 'The "accessToken" is invalid.'); + return authError<"check", 5>(5, 'The "accessToken" is invalid.'); + } + + const intercept = await internal.interceptEventCallbacks.check({ + accessToken, + refreshToken, + payload: { id: decodeAccessToken.payload.id }, + }); + + if (intercept.serverError) return authServerError(); + + if (intercept.intercepted) + return authError<"check", 9>( + 9, + "The check request was intercepted.", + intercept.interceptCode + ); + + return { + auth: { + error: false, + errorType: "method", + message: "Check successful.", + codes: { status: 0, intercept: 0 }, + }, + data: null, + }; + } catch (error) { + internal.log("warn", String(error)); + return authServerError(); + } + }; +} diff --git a/packages/server/src/methods/login.ts b/packages/server/src/methods/login.ts new file mode 100644 index 0000000..4411de3 --- /dev/null +++ b/packages/server/src/methods/login.ts @@ -0,0 +1,129 @@ +import { AuthInternal } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerMethod, + AuthServerUseEvents, +} from "../auth"; +import { authError, authServerError } from "../senders"; +import getUserByLogin from "../getUserByLogin"; +import { TokenPayload, generateToken } from "../tokenUtils"; + +//create login method +export function createLogin( + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): AuthServerMethod<"login"> { + return async ({ login, password }) => { + //login method is disabled + if (internal.config.methods.login === "disabled") { + internal.log("debug", "The login method is disabled."); + return authError<"login", 1>(1, "The login method is disabled."); + } + + try { + if (!login || !password) { + internal.log( + "debug", + 'The "login" ("email" or "username") or "password" is missing.' + ); + return authError<"login", 2>( + 2, + 'The "login" ("email" or "username") or "password" is missing.' + ); + } + + const getUserByLoginLogin = await getUserByLogin(login, internal); + + if (getUserByLoginLogin.serverError) return authServerError(); + + if (!getUserByLoginLogin.user) { + if (internal.config.sensitive.logs) + internal.log( + "debug", + 'The user was not found or the "password" is incorrect.' + ); + else internal.log("debug", "The user was not found."); + if (internal.config.sensitive.api) + return authError<"login", 5>( + 5, + 'The user was not found or the "password" is incorrect.' + ); + else return authError<"login", 3>(3, "The user was not found."); + } + + const checkPassword = await internal.useEventCallbacks.checkPassword({ + password: password, + hashedPassword: getUserByLoginLogin.user.hashedPassword, + }); + + if (checkPassword.serverError) return authServerError(); + + if (!checkPassword.matches) { + if (internal.config.sensitive.logs) + internal.log( + "debug", + 'The user was not found or the "password" is incorrect.' + ); + else internal.log("debug", 'The "password" is incorrect.'); + if (internal.config.sensitive.api) + return authError<"login", 5>( + 5, + 'The user was not found or the "password" is incorrect.' + ); + else return authError<"login", 4>(4, 'The "password" is incorrect.'); + } + + const payload: TokenPayload = { id: getUserByLoginLogin.user.id }; + + const refreshToken = generateToken( + payload, + internal.config.secrets.refreshToken + ); + + const accessToken = generateToken( + payload, + internal.config.secrets.accessToken, + internal.config.expiresIn + ); + + const intercept = await internal.interceptEventCallbacks.login({ + user: getUserByLoginLogin.user, + accessToken, + refreshToken, + payload, + }); + + if (intercept.serverError) return authServerError(); + + if (intercept.intercepted) + return authError<"login", 9>( + 9, + "The login request was intercepted.", + intercept.interceptCode + ); + + const storeToken = await internal.useEventCallbacks.storeToken({ + refreshToken, + }); + + if (storeToken.serverError) return authServerError(); + + return { + auth: { + error: false, + errorType: "method", + message: "Login successful.", + codes: { status: 0, intercept: 0 }, + }, + data: { accessToken, refreshToken }, + }; + } catch (error) { + internal.log("warn", String(error)); + return authServerError(); + } + }; +} diff --git a/packages/server/src/methods/logout.ts b/packages/server/src/methods/logout.ts new file mode 100644 index 0000000..2f3e307 --- /dev/null +++ b/packages/server/src/methods/logout.ts @@ -0,0 +1,87 @@ +import { AuthInternal } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerMethod, + AuthServerUseEvents, +} from "../auth"; +import { authError, authServerError } from "../senders"; +import { decodeToken } from "../tokenUtils"; + +//create logout method +export function createLogout( + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): AuthServerMethod<"logout"> { + return async ({ refreshToken }) => { + //logout method is disabled + if (internal.config.methods.logout === "disabled") { + internal.log("debug", "The logout method is disabled."); + return authError<"logout", 1>(1, "The logout method is disabled."); + } + + try { + if (!refreshToken) { + internal.log("debug", 'The "refreshToken" is missing.'); + return authError<"logout", 2>(2, 'The "refreshToken" is missing.'); + } + + const decodeRefreshToken = decodeToken( + refreshToken, + internal.config.secrets.refreshToken + ); + + if (!decodeRefreshToken.valid || !decodeRefreshToken.payload) { + internal.log("debug", 'The "refreshToken" is invalid.'); + return authError<"logout", 3>(3, 'The "refreshToken" is invalid.'); + } + + const checkToken = await internal.useEventCallbacks.checkToken({ + refreshToken, + }); + + if (checkToken.serverError) return authServerError(); + + if (!checkToken.exists) { + internal.log("debug", 'The "refreshToken" does not exist.'); + return authError<"logout", 4>(4, 'The "refreshToken" does not exist.'); + } + + const intercept = await internal.interceptEventCallbacks.logout({ + refreshToken, + payload: { id: decodeRefreshToken.payload.id }, + }); + + if (intercept.serverError) return authServerError(); + + if (intercept.intercepted) + return authError<"logout", 9>( + 9, + "The logout request was intercepted.", + intercept.interceptCode + ); + + const deleteToken = await internal.useEventCallbacks.deleteToken({ + refreshToken, + }); + + if (deleteToken.serverError) return authServerError(); + + return { + auth: { + error: false, + errorType: "method", + message: "Logout successful.", + codes: { status: 0, intercept: 0 }, + }, + data: null, + }; + } catch (error) { + internal.log("warn", String(error)); + return authServerError(); + } + }; +} diff --git a/packages/server/src/methods/refresh.ts b/packages/server/src/methods/refresh.ts new file mode 100644 index 0000000..106d106 --- /dev/null +++ b/packages/server/src/methods/refresh.ts @@ -0,0 +1,87 @@ +import { AuthInternal } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerMethod, + AuthServerUseEvents, +} from "../auth"; +import { authError, authServerError } from "../senders"; +import { decodeToken, generateToken } from "../tokenUtils"; + +//create refresh method +export function createRefresh( + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): AuthServerMethod<"refresh"> { + return async ({ refreshToken }) => { + //refresh method is disabled + if (internal.config.methods.refresh === "disabled") { + internal.log("debug", "The refresh method is disabled."); + return authError<"refresh", 1>(1, "The refresh method is disabled."); + } + + try { + if (!refreshToken) { + internal.log("debug", 'The "refreshToken" is missing.'); + return authError<"refresh", 2>(2, 'The "refreshToken" is missing.'); + } + + const decodeRefreshToken = decodeToken( + refreshToken, + internal.config.secrets.refreshToken + ); + + if (!decodeRefreshToken.valid || !decodeRefreshToken.payload) { + internal.log("debug", 'The "refreshToken" is invalid.'); + return authError<"refresh", 3>(3, 'The "refreshToken" is invalid.'); + } + + const checkToken = await internal.useEventCallbacks.checkToken({ + refreshToken, + }); + + if (checkToken.serverError) return authServerError(); + + if (!checkToken.exists) { + internal.log("debug", 'The "refreshToken" does not exist.'); + return authError<"refresh", 4>(4, 'The "refreshToken" does not exist.'); + } + + const intercept = await internal.interceptEventCallbacks.refresh({ + refreshToken, + payload: { id: decodeRefreshToken.payload.id }, + }); + + if (intercept.serverError) return authServerError(); + + if (intercept.intercepted) + return authError<"refresh", 9>( + 9, + "The refresh request was intercepted.", + intercept.interceptCode + ); + + const accessToken = generateToken( + { id: decodeRefreshToken.payload.id }, + internal.config.secrets.accessToken, + internal.config.expiresIn + ); + + return { + auth: { + error: false, + errorType: "method", + message: "Refresh successful.", + codes: { status: 0, intercept: 0 }, + }, + data: { accessToken }, + }; + } catch (error) { + internal.log("warn", String(error)); + return authServerError(); + } + }; +} diff --git a/packages/server/src/methods/register.ts b/packages/server/src/methods/register.ts new file mode 100644 index 0000000..ecdc4c8 --- /dev/null +++ b/packages/server/src/methods/register.ts @@ -0,0 +1,146 @@ +import { AuthInternal, User } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerMethod, + AuthServerUseEvents, +} from "../auth"; +import { authError, authServerError } from "../senders"; +import getUserByLogin from "../getUserByLogin"; + +//create register method +export function createRegister( + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): AuthServerMethod<"register"> { + return async ({ email, username, password }) => { + //register method is disabled + if (internal.config.methods.register === "disabled") { + internal.log("debug", "The registration method is disabled."); + return authError<"register", 1>( + 1, + "The registration method is disabled." + ); + } + + try { + if (!email || !username || !password) { + internal.log( + "debug", + 'The "email", "username" or "password" is missing.' + ); + return authError<"register", 2>( + 2, + 'The "email", "username" or "password" is missing.' + ); + } + + const validateMail = await internal.useEventCallbacks.validateMail({ + email, + }); + + if (validateMail.serverError) return authServerError(); + + if (!validateMail.isValid) { + internal.log("debug", 'The "email" is malformed.'); + return authError<"register", 3>(3, 'The "email" is malformed.'); + } + + const validatePassword = + await internal.useEventCallbacks.validatePassword({ + password, + }); + + if (validatePassword.serverError) return authServerError(); + + if (!validatePassword.isValid) { + internal.log("debug", 'The "password" is too weak.'); + return authError<"register", 4>(4, 'The "password" is too weak.'); + } + + const getUserByLoginEmail = await getUserByLogin(email, internal); + + if (getUserByLoginEmail.serverError) return authServerError(); + + if (getUserByLoginEmail.user) { + if (internal.config.sensitive.logs) + internal.log("debug", 'The "login" is already in use.'); + else internal.log("debug", 'The "email" is already in use.'); + if (internal.config.sensitive.api) + return authError<"register", 7>(7, 'The "login" is already in use.'); + else + return authError<"register", 5>(5, 'The "email" is already in use.'); + } + + const getUserByLoginName = await getUserByLogin(username, internal); + + if (getUserByLoginName.serverError) return authServerError(); + + if (getUserByLoginName.user) { + if (internal.config.sensitive.logs) + internal.log("debug", 'The "login" is already in use.'); + else internal.log("debug", 'The "username" is already in use.'); + if (internal.config.sensitive.api) + return authError<"register", 7>(7, 'The "login" is already in use.'); + else + return authError<"register", 6>( + 6, + 'The "username" is already in use.' + ); + } + + const hashPassword = await internal.useEventCallbacks.hashPassword({ + password, + }); + + if (hashPassword.serverError) return authServerError(); + + const genId = await internal.useEventCallbacks.genId({ + email, + username, + }); + + if (genId.serverError) return authServerError(); + + const user: User<"id" | "email" | "username" | "hashedPassword"> = { + id: genId.id, + email, + username, + hashedPassword: hashPassword.hashedPassword, + }; + + const intercept = await internal.interceptEventCallbacks.register({ + user, + }); + + if (intercept.serverError) return authServerError(); + + if (intercept.intercepted) + return authError<"register", 9>( + 9, + "The registration request was intercepted.", + intercept.interceptCode + ); + + const storeUser = await internal.useEventCallbacks.storeUser({ user }); + + if (storeUser.serverError) return authServerError(); + + return { + auth: { + error: false, + errorType: "method", + message: "Registration successful.", + codes: { status: 0, intercept: 0 }, + }, + data: { id: user.id, email: user.email, username: user.username }, + }; + } catch (error) { + internal.log("warn", String(error)); + return authServerError(); + } + }; +} diff --git a/packages/server/src/methods/validate.ts b/packages/server/src/methods/validate.ts new file mode 100644 index 0000000..7a6bde3 --- /dev/null +++ b/packages/server/src/methods/validate.ts @@ -0,0 +1,56 @@ +import { AuthInternal } from "@auth-tools/base"; +import { + AuthServerConfig, + AuthServerInterceptEvents, + AuthServerMethod, + AuthServerUseEvents, +} from "../auth"; +import { authError, authServerError } from "../senders"; +import { decodeToken } from "../tokenUtils"; + +//create validate method +export function createValidate( + internal: AuthInternal< + AuthServerConfig, + AuthServerUseEvents, + AuthServerInterceptEvents + > +): AuthServerMethod<"validate"> { + return async ({ accessToken }) => { + //validate method is disabled + if (internal.config.methods.validate === "disabled") { + internal.log("debug", "The validate method is disabled."); + return authError<"validate", 1>(1, "The validation method is disabled."); + } + + try { + if (!accessToken) { + internal.log("debug", 'The "accessToken" is missing.'); + return authError<"validate", 2>(2, 'The "accessToken" is missing.'); + } + + const decodeAccessToken = decodeToken( + accessToken, + internal.config.secrets.accessToken + ); + + if (!decodeAccessToken.valid || !decodeAccessToken.payload) { + internal.log("debug", 'The "accessToken" is invalid.'); + return authError<"validate", 3>(3, 'The "accessToken" is invalid.'); + } + + return { + auth: { + error: false, + errorType: "method", + message: "Validation successful.", + codes: { status: 0, intercept: 0 }, + }, + data: { id: decodeAccessToken.payload.id }, + }; + } catch (error) { + internal.log("warn", String(error)); + return authServerError(); + } + }; +} diff --git a/packages/server/src/senders.ts b/packages/server/src/senders.ts new file mode 100644 index 0000000..9110efc --- /dev/null +++ b/packages/server/src/senders.ts @@ -0,0 +1,35 @@ +import { AuthMessages, AuthProtocol, AuthResponse } from "@auth-tools/base"; + +export function authError< + Method extends keyof AuthProtocol, + StatusCode extends number +>( + statusCode: StatusCode, + message: AuthMessages[`${Method}_${StatusCode}`], + interceptCode: number = 0 +): AuthResponse { + return { + auth: { + error: true, + errorType: "method", + message: message, + codes: { + status: statusCode, + intercept: interceptCode, + }, + }, + data: null, + } as AuthResponse; +} + +export function authServerError(): AuthProtocol[keyof AuthProtocol]["responses"]["server_5"] { + return { + auth: { + error: true, + errorType: "server", + message: "An error occurred on the server. Please try again later.", + codes: { status: 5, intercept: 0 }, + }, + data: null, + }; +} diff --git a/packages/server/src/tokenUtils.ts b/packages/server/src/tokenUtils.ts new file mode 100644 index 0000000..1276bfe --- /dev/null +++ b/packages/server/src/tokenUtils.ts @@ -0,0 +1,36 @@ +import { User } from "@auth-tools/base"; +import { sign, verify } from "jsonwebtoken"; + +//payload of the token based on User type +//id only, because username and email could change +export type TokenPayload = User<"id">; + +//generare an access- or refreshToken +export function generateToken( + payload: TokenPayload, + secret: string, + expiresIn?: number +) { + //gemerate (sign) a token with the payload from secret + return sign( + payload, + secret, + //add expiresIn flag when given (only used for accessTokens) + expiresIn ? { expiresIn: expiresIn } : undefined + ); +} + +//decode the payload of a token (also verify it is not modified) +export function decodeToken( + token: string, + secret: string +): { valid: boolean; payload: TokenPayload | null } { + try { + //decrypt the token with secret + const data = verify(token, secret) as TokenPayload; + return { valid: true, payload: { id: data.id } }; + } catch { + //return unvalid, when token decryption failed + return { valid: false, payload: null }; + } +} diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..ce1e038 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2016", + "lib": ["ES2016"], + "module": "CommonJS", + "moduleResolution": "Node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/template/package.json b/packages/template/package.json index 37a86c8..752af62 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -9,7 +9,7 @@ "private": false, "scripts": { "build": "npm run build:remove && tsc", - "build:remove": "rimraf build", + "build:remove": "rimraf dist", "local": "npm run build && ts-node-dev --respawn local/index.ts", "prepublish": "npm run build", "remove": "npm run build:remove && rimraf node_modules yarn.lock package-lock.json pnpm-lock.yaml"