added authorization stores and fixed http client

This commit is contained in:
Fran Jurmanović
2021-05-29 17:11:21 +02:00
parent f835c76b39
commit f0d2e7b06d
23 changed files with 473 additions and 24 deletions

View File

@@ -22,6 +22,7 @@
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"validator": "^13.6.0",
"webpack": "^5.38.1", "webpack": "^5.38.1",
"webpack-dev-server": "^3.11.2" "webpack-dev-server": "^3.11.2"
}, },

View File

@@ -2,16 +2,24 @@ import { attr, targets, controller, target } from "@github/catalyst";
import { closest, index, update, isTrue } from "core/utils"; import { closest, index, update, isTrue } from "core/utils";
import { html, render, until } from "@github/jtml"; import { html, render, until } from "@github/jtml";
import { PingService } from "services/"; import { PingService } from "services/";
import { RouterService } from "core/services"; import { AppService, HttpClient, RouterService } from "core/services";
import { AuthStore } from "core/store";
@controller @controller
class AppMainElement extends HTMLElement { class AppMainElement extends HTMLElement {
public routerService: RouterService; public routerService: RouterService;
public authStore: AuthStore;
private httpClient: HttpClient;
public appService: AppService;
constructor() { constructor() {
super(); super();
} }
connectedCallback() { connectedCallback() {
this.httpClient = new HttpClient();
this.appService = new AppService(this, this.httpClient);
this.routerService = new RouterService(this); this.routerService = new RouterService(this);
this.authStore = new AuthStore(this.appService);
this.routerService.setRoutes([ this.routerService.setRoutes([
{ {
path: "/", path: "/",
@@ -21,10 +29,30 @@ class AppMainElement extends HTMLElement {
{ {
path: "/home", path: "/home",
component: "home-page", 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(); this.routerService.init();
} }
isAuth = () => {
if (this.authStore?.token == null) {
this.routerService.goTo("/unauthorized");
return true;
}
};
} }
export type { AppMainElement }; export type { AppMainElement };

View File

@@ -1,2 +1,3 @@
export * from "./app-main/AppMainElement"; export * from "./app-main/AppMainElement";
export * from "./app-link/AppLinkElement"; export * from "./app-link/AppLinkElement";
export * from "./input-field/InputFieldElement";

View File

@@ -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`<span data-target="input-field.main">
${this.label &&
html`<label for="${this.randId}">${this.label}</label>`}
<input type="${this.type}" data-target="input-field.inp" />
${this.error && html`<span>${this.error}</span>`}
</span>`,
this
);
}
}
export type { InputFieldElement };

View File

@@ -0,0 +1 @@
export * from "./validatorErrors";

View File

@@ -0,0 +1,4 @@
export const validatorErrors = {
required: "{- name} is required.",
isEmail: "{- name} needs to be email format.",
};

View File

@@ -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<any> => {
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<any> => {
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<any> => {
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<any> => {
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;

View File

@@ -1,29 +1,26 @@
import { HttpClient } from "core/services"; import { AppService, HttpClient } from "core/services";
class BaseService { class BaseService {
private httpClient: HttpClient; constructor(public endpoint: string, public appService: AppService) {}
constructor(private endpoint: string) {
this.httpClient = new HttpClient();
}
getAll = (headers?: HeadersInit) => { getAll = (headers?: HeadersInit) => {
return this.httpClient.get(this.endpoint, null, headers); return this.appService.get(this.endpoint, null, headers);
}; };
get = (params?: Object, headers?: HeadersInit) => { 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) => { 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) => { 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) => { delete = (data?: Object, headers?: HeadersInit) => {
return this.httpClient.delete(this.endpoint, data, headers); return this.appService.delete(this.endpoint, data, headers);
}; };
} }

View File

@@ -77,7 +77,13 @@ export default HttpClient;
async function createRequest(request: Request): Promise<Response> { async function createRequest(request: Request): Promise<Response> {
let response: Response = await fetch(request); 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}`); throw new Error(`HTTP error! status: ${response.status}`);
} else { } else {
if (response.headers.get("Content-Type") !== null) { if (response.headers.get("Content-Type") !== null) {

View File

@@ -1,3 +1,4 @@
export { default as HttpClient } from "./http-service/HttpClient"; export { default as HttpClient } from "./http-service/HttpClient";
export { default as BaseService } from "./base-service/BaseService"; export { default as BaseService } from "./base-service/BaseService";
export { default as RouterService } from "./router-service/RouterService"; export { default as RouterService } from "./router-service/RouterService";
export { default as AppService } from "./app-service/AppService";

View File

@@ -17,12 +17,13 @@ class RouterService {
setRoutes = (routes: Array<any>) => { setRoutes = (routes: Array<any>) => {
if (!Array.isArray(this._routes)) this._routes = []; if (!Array.isArray(this._routes)) this._routes = [];
routes.forEach((route) => { routes.forEach((route) => {
const { path, component, data, layout } = route; const { path, component, data, layout, middleware } = route;
const _routeState: RouteState = new RouteState( const _routeState: RouteState = new RouteState(
path, path,
component, component,
data, data,
layout layout,
middleware
); );
this._routes?.push(_routeState); this._routes?.push(_routeState);
}); });
@@ -30,10 +31,13 @@ class RouterService {
update() { update() {
if (!this._routes) return; if (!this._routes) return;
const path = window.location.pathname;
const _mainRoot = this.mainRoot;
for (const route of this._routes) { for (const route of this._routes) {
const path = window.location.pathname;
if (path == route.path) { if (path == route.path) {
const _mainRoot = this.mainRoot; if (route.middleware && typeof route.middleware == "function") {
if (route.middleware()) return;
}
let changed: boolean = false; let changed: boolean = false;
if (_mainRoot?.childNodes.length > 0) { if (_mainRoot?.childNodes.length > 0) {
console.log(_mainRoot.childNodes); console.log(_mainRoot.childNodes);
@@ -94,6 +98,7 @@ class RouterService {
return; return;
} }
} }
_mainRoot.innerHTML = "404 - Not found";
} }
@update @update
@@ -101,13 +106,27 @@ class RouterService {
if (!Array.isArray(this.historyStack)) this.historyStack = []; if (!Array.isArray(this.historyStack)) this.historyStack = [];
const _index = this._routes.findIndex((route) => route.path === path); const _index = this._routes.findIndex((route) => route.path === path);
if (_index >= 0) { 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()); const url = new URL(window.location.toString());
url.pathname = path; url.pathname = path;
window.history.pushState({}, "", url.toString()); 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 @update
init() {} init() {}
} }
@@ -117,7 +136,8 @@ class RouteState {
public path: string, public path: string,
public component: string, public component: string,
public data: any, public data: any,
public layout: string public layout: string,
public middleware: any
) {} ) {}
} }

View File

@@ -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;

1
src/core/store/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as AuthStore } from "./AuthStore";

View File

@@ -0,0 +1,4 @@
export default function firstUpper(s: string): string {
if (typeof s !== "string") return "";
return s.charAt(0).toUpperCase() + s.slice(1);
}

View File

@@ -3,3 +3,4 @@ export { default as update } from "./update-deco";
export { default as index } from "./index-deco"; export { default as index } from "./index-deco";
export { default as closest } from "./closest-deco"; export { default as closest } from "./closest-deco";
export { default as isTrue } from "./isTrue"; export { default as isTrue } from "./isTrue";
export { default as firstUpper } from "./first-upper";

View File

@@ -0,0 +1,3 @@
export default function randomId() {
return "_" + Math.random().toString(36).substr(2, 5);
}

View File

@@ -2,21 +2,25 @@ import { attr, targets, controller, target } from "@github/catalyst";
import { closest, index, update, isTrue } from "core/utils"; import { closest, index, update, isTrue } from "core/utils";
import { html, render, until } from "@github/jtml"; import { html, render, until } from "@github/jtml";
import { PingService } from "services/"; import { PingService } from "services/";
import { AppMainElement } from "components/";
@controller @controller
class HomePageElement extends HTMLElement { class HomePageElement extends HTMLElement {
private pingService: PingService; private pingService: PingService;
@closest appMain: AppMainElement;
constructor() { constructor() {
super(); super();
this.pingService = new PingService();
} }
@update @update
connectedCallback() {} connectedCallback() {
this.pingService = new PingService(this.appMain?.appService);
this.getPong();
}
getPong = async () => { getPong = async () => {
try { try {
const response = await this.pingService.getAll(); const response = await this.pingService.getAll();
return response.api; console.log(response);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }

View File

@@ -1 +1,2 @@
export * from "./home-page/HomePageElement"; export * from "./home-page/HomePageElement";
export * from "./register-page/RegisterPageElement";

View File

@@ -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<InputFieldElement>;
@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`
<form>
<input-field
data-type="email"
data-name="email"
data-label="E-mail"
data-targets="register-page.inputs"
data-rules="required|isEmail"
></input-field>
<input-field
data-type="password"
data-name="password"
data-label="Password"
data-targets="register-page.inputs"
>
</input-field>
<button
type="button"
data-action="click:register-page#onSubmit"
>
Register
</button>
</form>
`,
this
);
}
}

View File

@@ -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;

View File

@@ -1,8 +1,8 @@
import { BaseService } from "core/services"; import { AppService, BaseService } from "core/services";
class PingService extends BaseService { class PingService extends BaseService {
constructor() { constructor(appService: AppService) {
super("/api"); super("/wallet", appService);
} }
} }

View File

@@ -1 +1,2 @@
export { default as PingService } from "./PingService"; export { default as PingService } from "./PingService";
export { default as AuthService } from "./AuthService";

View File

@@ -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" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== 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: vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"