diff --git a/package.json b/package.json index 7d7bfa3..ceb3143 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "babel-polyfill": "^6.26.0", "connect-history-api-fallback": "^1.6.0", "html-webpack-plugin": "^5.3.1", + "validator": "^13.6.0", "webpack": "^5.38.1", "webpack-dev-server": "^3.11.2" }, diff --git a/src/components/app-main/AppMainElement.ts b/src/components/app-main/AppMainElement.ts index 488dd49..bb631b5 100644 --- a/src/components/app-main/AppMainElement.ts +++ b/src/components/app-main/AppMainElement.ts @@ -2,16 +2,24 @@ import { attr, targets, controller, target } from "@github/catalyst"; import { closest, index, update, isTrue } from "core/utils"; import { html, render, until } from "@github/jtml"; import { PingService } from "services/"; -import { RouterService } from "core/services"; +import { AppService, HttpClient, RouterService } from "core/services"; +import { AuthStore } from "core/store"; @controller class AppMainElement extends HTMLElement { public routerService: RouterService; + public authStore: AuthStore; + private httpClient: HttpClient; + public appService: AppService; + constructor() { super(); } connectedCallback() { + this.httpClient = new HttpClient(); + this.appService = new AppService(this, this.httpClient); this.routerService = new RouterService(this); + this.authStore = new AuthStore(this.appService); this.routerService.setRoutes([ { path: "/", @@ -21,10 +29,30 @@ class AppMainElement extends HTMLElement { { path: "/home", component: "home-page", + middleware: this.isAuth, + }, + { + path: "/rb", + component: "register-page", + }, + { + path: "/unauthorized", + component: "register-page", + }, + { + path: "token-expired", + component: "register-page", }, ]); this.routerService.init(); } + + isAuth = () => { + if (this.authStore?.token == null) { + this.routerService.goTo("/unauthorized"); + return true; + } + }; } export type { AppMainElement }; diff --git a/src/components/index.ts b/src/components/index.ts index 6e9857d..c4c598e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,3 @@ export * from "./app-main/AppMainElement"; export * from "./app-link/AppLinkElement"; +export * from "./input-field/InputFieldElement"; diff --git a/src/components/input-field/InputFieldElement.ts b/src/components/input-field/InputFieldElement.ts new file mode 100644 index 0000000..8486187 --- /dev/null +++ b/src/components/input-field/InputFieldElement.ts @@ -0,0 +1,81 @@ +import { attr, targets, controller, target } from "@github/catalyst"; +import { closest, index, update, isTrue, firstUpper } from "core/utils"; +import { html, render, until } from "@github/jtml"; +import { PingService } from "services/"; +import { AppMainElement } from "components/app-main/AppMainElement"; +import { RouterService } from "core/services"; +import randomId from "core/utils/random-id"; +import validator from "validator"; +import { validatorErrors } from "core/constants"; + +@controller +class InputFieldElement extends HTMLElement { + @closest appMain: AppMainElement; + @attr name: string; + @attr type: string; + @attr label: string; + @attr rules: string; + @target main: HTMLElement; + @target inp: HTMLElement; + error: string; + randId: string; + routerService: RouterService; + constructor() { + super(); + } + + @update + public connectedCallback(): void { + this.randId = `${name}${randomId()}`; + } + + get valid(): boolean { + return !!this.error; + } + + @update + validate(): boolean { + let _return = true; + const rules = this.rules?.split("|").filter((a) => a); + const value = (this.inp as HTMLInputElement)?.value; + rules + .slice() + .reverse() + .forEach((rule) => { + let valid = true; + if (rule == "required") { + if (value === "") valid = false; + } else { + if (validator.hasOwnProperty(rule)) { + valid = validator?.[rule]?.(value); + } + } + if (!valid) { + const error = validatorErrors[rule]?.replaceAll( + "{- name}", + firstUpper(this.name?.toString()) + ); + _return = false; + this.error = error; + } + }); + if (_return) { + this.error = null; + } + return _return; + } + + update() { + render( + html` + ${this.label && + html``} + + ${this.error && html`${this.error}`} + `, + this + ); + } +} + +export type { InputFieldElement }; diff --git a/src/core/constants/index.ts b/src/core/constants/index.ts new file mode 100644 index 0000000..a5c884f --- /dev/null +++ b/src/core/constants/index.ts @@ -0,0 +1 @@ +export * from "./validatorErrors"; diff --git a/src/core/constants/validatorErrors.ts b/src/core/constants/validatorErrors.ts new file mode 100644 index 0000000..f9e1659 --- /dev/null +++ b/src/core/constants/validatorErrors.ts @@ -0,0 +1,4 @@ +export const validatorErrors = { + required: "{- name} is required.", + isEmail: "{- name} needs to be email format.", +}; diff --git a/src/core/services/app-service/AppService.ts b/src/core/services/app-service/AppService.ts new file mode 100644 index 0000000..818bc81 --- /dev/null +++ b/src/core/services/app-service/AppService.ts @@ -0,0 +1,135 @@ +import { AppMainElement } from "components/"; +import { HttpClient } from ".."; + +class AppService { + constructor( + public appMain: AppMainElement, + public httpClient: HttpClient + ) {} + + post = async ( + url: string, + data: Object, + headersParam: HeadersInit + ): Promise => { + headersParam = { + ...headersParam, + Authorization: `BEARER ${this.appMain?.authStore?.token}`, + }; + try { + const response = await this.httpClient.post( + url, + data, + headersParam + ); + if ( + response?.statusCode == 400 || + response?.statusCode == 500 || + response?.statusCode == 401 + ) { + if (response?.statusCode == 401) { + this.appMain.authStore.token = null; + this.appMain.routerService.goTo("/token-expired"); + } + throw response; + } + return response; + } catch (err) { + throw err; + } + }; + + put = async ( + url: string, + data: Object, + headersParam: HeadersInit + ): Promise => { + headersParam = { + ...headersParam, + Authorization: `BEARER ${this.appMain?.authStore?.token}`, + }; + try { + const response = await this.httpClient.put(url, data, headersParam); + if ( + response?.statusCode == 400 || + response?.statusCode == 500 || + response?.statusCode == 401 + ) { + if (response?.statusCode == 401) { + this.appMain.authStore.token = null; + this.appMain.routerService.goTo("/token-expired"); + } + throw response; + } + return response; + } catch (err) { + throw err; + } + }; + + delete = async ( + url: string, + data: Object, + headersParam: HeadersInit + ): Promise => { + headersParam = { + ...headersParam, + Authorization: `BEARER ${this.appMain?.authStore?.token}`, + }; + try { + const response = await this.httpClient.delete( + url, + data, + headersParam + ); + if ( + response?.statusCode == 400 || + response?.statusCode == 500 || + response?.statusCode == 401 + ) { + if (response?.statusCode == 401) { + this.appMain.authStore.token = null; + this.appMain.routerService.goTo("/token-expired"); + } + throw response; + } + return response; + } catch (err) { + throw err; + } + }; + + get = async ( + url: string, + params: Object, + headersParam: HeadersInit + ): Promise => { + headersParam = { + ...headersParam, + Authorization: `BEARER ${this.appMain?.authStore?.token}`, + }; + try { + const response = await this.httpClient.get( + url, + params, + headersParam + ); + if ( + response?.statusCode == 400 || + response?.statusCode == 500 || + response?.statusCode == 401 + ) { + if (response?.statusCode == 401) { + this.appMain.authStore.token = null; + this.appMain.routerService.goTo("/token-expired"); + } + throw response; + } + return response; + } catch (err) { + throw err; + } + }; +} + +export default AppService; diff --git a/src/core/services/base-service/BaseService.ts b/src/core/services/base-service/BaseService.ts index 9fb727b..94f2890 100644 --- a/src/core/services/base-service/BaseService.ts +++ b/src/core/services/base-service/BaseService.ts @@ -1,29 +1,26 @@ -import { HttpClient } from "core/services"; +import { AppService, HttpClient } from "core/services"; class BaseService { - private httpClient: HttpClient; - constructor(private endpoint: string) { - this.httpClient = new HttpClient(); - } + constructor(public endpoint: string, public appService: AppService) {} getAll = (headers?: HeadersInit) => { - return this.httpClient.get(this.endpoint, null, headers); + return this.appService.get(this.endpoint, null, headers); }; get = (params?: Object, headers?: HeadersInit) => { - return this.httpClient.get(this.endpoint, params, headers); + return this.appService.get(this.endpoint, params, headers); }; put = (data?: Object, headers?: HeadersInit) => { - return this.httpClient.put(this.endpoint, data, headers); + return this.appService.put(this.endpoint, data, headers); }; post = (data?: Object, headers?: HeadersInit) => { - return this.httpClient.post(this.endpoint, data, headers); + return this.appService.post(this.endpoint, data, headers); }; delete = (data?: Object, headers?: HeadersInit) => { - return this.httpClient.delete(this.endpoint, data, headers); + return this.appService.delete(this.endpoint, data, headers); }; } diff --git a/src/core/services/http-service/HttpClient.ts b/src/core/services/http-service/HttpClient.ts index 304b8c7..4bfe777 100644 --- a/src/core/services/http-service/HttpClient.ts +++ b/src/core/services/http-service/HttpClient.ts @@ -77,7 +77,13 @@ export default HttpClient; async function createRequest(request: Request): Promise { let response: Response = await fetch(request); - if (!response.ok && response.status !== 403 && response.status !== 400) { + if ( + !response.ok && + response.status !== 403 && + response.status !== 400 && + response.status !== 401 && + response.status !== 500 + ) { throw new Error(`HTTP error! status: ${response.status}`); } else { if (response.headers.get("Content-Type") !== null) { diff --git a/src/core/services/index.ts b/src/core/services/index.ts index 47095cf..9b07a3d 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -1,3 +1,4 @@ export { default as HttpClient } from "./http-service/HttpClient"; export { default as BaseService } from "./base-service/BaseService"; export { default as RouterService } from "./router-service/RouterService"; +export { default as AppService } from "./app-service/AppService"; diff --git a/src/core/services/router-service/RouterService.ts b/src/core/services/router-service/RouterService.ts index e134f34..439ecc4 100644 --- a/src/core/services/router-service/RouterService.ts +++ b/src/core/services/router-service/RouterService.ts @@ -17,12 +17,13 @@ class RouterService { setRoutes = (routes: Array) => { if (!Array.isArray(this._routes)) this._routes = []; routes.forEach((route) => { - const { path, component, data, layout } = route; + const { path, component, data, layout, middleware } = route; const _routeState: RouteState = new RouteState( path, component, data, - layout + layout, + middleware ); this._routes?.push(_routeState); }); @@ -30,10 +31,13 @@ class RouterService { update() { if (!this._routes) return; + const path = window.location.pathname; + const _mainRoot = this.mainRoot; for (const route of this._routes) { - const path = window.location.pathname; if (path == route.path) { - const _mainRoot = this.mainRoot; + if (route.middleware && typeof route.middleware == "function") { + if (route.middleware()) return; + } let changed: boolean = false; if (_mainRoot?.childNodes.length > 0) { console.log(_mainRoot.childNodes); @@ -94,6 +98,7 @@ class RouterService { return; } } + _mainRoot.innerHTML = "404 - Not found"; } @update @@ -101,13 +106,27 @@ class RouterService { if (!Array.isArray(this.historyStack)) this.historyStack = []; const _index = this._routes.findIndex((route) => route.path === path); if (_index >= 0) { - this.historyStack.push(this._routes[_index]); + const newRoute = this._routes[_index]; + this.historyStack.push(newRoute); const url = new URL(window.location.toString()); url.pathname = path; window.history.pushState({}, "", url.toString()); } } + @update + goBack() { + if (!Array.isArray(this.historyStack)) this.historyStack = []; + const lenHistory = this.historyStack.length; + if (lenHistory > 1) { + const nextRoute = this.historyStack[lenHistory - 2]; + const url = new URL(window.location.toString()); + url.pathname = nextRoute.path; + window.history.pushState({}, "", url.toString()); + this.historyStack.pop(); + } + } + @update init() {} } @@ -117,7 +136,8 @@ class RouteState { public path: string, public component: string, public data: any, - public layout: string + public layout: string, + public middleware: any ) {} } diff --git a/src/core/store/AuthStore.ts b/src/core/store/AuthStore.ts new file mode 100644 index 0000000..1c2d258 --- /dev/null +++ b/src/core/store/AuthStore.ts @@ -0,0 +1,54 @@ +import { AppService } from "core/services"; +import { AuthService } from "services/"; + +class AuthStore { + private _token; + private _userDetails; + private authService: AuthService; + constructor(private appService: AppService) { + this.token = localStorage.getItem("token"); + this.authService = new AuthService(this.appService); + } + + get token() { + return this._token; + } + + set token(token) { + this._token = token; + localStorage.setItem("token", token); + } + + get user() { + return this._userDetails; + } + + set user(userDetails) { + this._userDetails = userDetails; + } + + userLogin = async (formObject) => { + try { + const response = await this.authService.login(formObject); + if (response?.token) { + this.token = response.token; + } else { + this.token = null; + localStorage.removeItem("token"); + } + return response; + } catch (err) { + throw err; + } + }; + + userRegister = async (formObject) => { + try { + await this.authService.register(formObject); + } catch (err) { + throw err; + } + }; +} + +export default AuthStore; diff --git a/src/core/store/index.ts b/src/core/store/index.ts new file mode 100644 index 0000000..e97a1fe --- /dev/null +++ b/src/core/store/index.ts @@ -0,0 +1 @@ +export { default as AuthStore } from "./AuthStore"; diff --git a/src/core/utils/first-upper.ts b/src/core/utils/first-upper.ts new file mode 100644 index 0000000..9531212 --- /dev/null +++ b/src/core/utils/first-upper.ts @@ -0,0 +1,4 @@ +export default function firstUpper(s: string): string { + if (typeof s !== "string") return ""; + return s.charAt(0).toUpperCase() + s.slice(1); +} diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index b10241f..6ff2da0 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -3,3 +3,4 @@ export { default as update } from "./update-deco"; export { default as index } from "./index-deco"; export { default as closest } from "./closest-deco"; export { default as isTrue } from "./isTrue"; +export { default as firstUpper } from "./first-upper"; diff --git a/src/core/utils/random-id.ts b/src/core/utils/random-id.ts new file mode 100644 index 0000000..713f9e5 --- /dev/null +++ b/src/core/utils/random-id.ts @@ -0,0 +1,3 @@ +export default function randomId() { + return "_" + Math.random().toString(36).substr(2, 5); +} diff --git a/src/pages/home-page/HomePageElement.ts b/src/pages/home-page/HomePageElement.ts index 03c0585..9b35b8c 100644 --- a/src/pages/home-page/HomePageElement.ts +++ b/src/pages/home-page/HomePageElement.ts @@ -2,21 +2,25 @@ import { attr, targets, controller, target } from "@github/catalyst"; import { closest, index, update, isTrue } from "core/utils"; import { html, render, until } from "@github/jtml"; import { PingService } from "services/"; +import { AppMainElement } from "components/"; @controller class HomePageElement extends HTMLElement { private pingService: PingService; + @closest appMain: AppMainElement; constructor() { super(); - this.pingService = new PingService(); } @update - connectedCallback() {} + connectedCallback() { + this.pingService = new PingService(this.appMain?.appService); + this.getPong(); + } getPong = async () => { try { const response = await this.pingService.getAll(); - return response.api; + console.log(response); } catch (err) { console.log(err); } diff --git a/src/pages/index.ts b/src/pages/index.ts index bcef41b..7075d09 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1 +1,2 @@ export * from "./home-page/HomePageElement"; +export * from "./register-page/RegisterPageElement"; diff --git a/src/pages/register-page/RegisterPageElement.ts b/src/pages/register-page/RegisterPageElement.ts new file mode 100644 index 0000000..fb12534 --- /dev/null +++ b/src/pages/register-page/RegisterPageElement.ts @@ -0,0 +1,85 @@ +import { attr, targets, controller, target } from "@github/catalyst"; +import { closest, index, update, isTrue } from "core/utils"; +import { html, render, until } from "@github/jtml"; +import { AuthService, PingService } from "services/"; +import { AppMainElement, InputFieldElement } from "components/"; + +@controller +class RegisterPageElement extends HTMLElement { + @targets inputs: Array; + @closest appMain: AppMainElement; + authService: AuthService; + constructor() { + super(); + } + @update + connectedCallback() { + this.authService = new AuthService(this.appMain.appService); + } + + get values(): Object { + const formObject = {}; + this.inputs.forEach((input: InputFieldElement) => { + const inputType = input.inp; + formObject[input.name] = (inputType as HTMLInputElement).value; + }); + return formObject; + } + + onSubmit = async () => { + try { + if (!this.validate()) { + return; + } + const response = await this.appMain.authStore.userLogin( + this.values + ); + console.log(response); + + if (response?.token) { + this.appMain.routerService.goTo("/"); + } + } catch (err) { + console.log(err); + } + }; + + validate(): boolean { + let _return = true; + this.inputs.forEach((input: InputFieldElement) => { + const valid: boolean = input.validate(); + if (!valid) _return = false; + }); + return _return; + } + + update() { + render( + html` +
+ + + + +
+ `, + this + ); + } +} diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..1828f04 --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,15 @@ +import { AppService, BaseService } from "core/services"; + +class PingService extends BaseService { + constructor(appService: AppService) { + super("/auth", appService); + } + login = (data?: Object, headers?: HeadersInit) => { + return this.appService.post(this.endpoint + "/login", data, headers); + }; + register = (data?: Object, headers?: HeadersInit) => { + return this.appService.post(this.endpoint + "/register", data, headers); + }; +} + +export default PingService; diff --git a/src/services/PingService.ts b/src/services/PingService.ts index cad4809..0a7f6ce 100644 --- a/src/services/PingService.ts +++ b/src/services/PingService.ts @@ -1,8 +1,8 @@ -import { BaseService } from "core/services"; +import { AppService, BaseService } from "core/services"; class PingService extends BaseService { - constructor() { - super("/api"); + constructor(appService: AppService) { + super("/wallet", appService); } } diff --git a/src/services/index.ts b/src/services/index.ts index c9327e0..1c63abb 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1,2 @@ export { default as PingService } from "./PingService"; +export { default as AuthService } from "./AuthService"; diff --git a/yarn.lock b/yarn.lock index f357d50..f9a7952 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4358,6 +4358,11 @@ v8-compile-cache@^2.2.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +validator@^13.6.0: + version "13.6.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059" + integrity sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"