mirror of
https://github.com/Nezumi-2711/typescript-training.git
synced 2026-09-22 13:48:29 +00:00
Merge pull request #7 from Nez27/feat/add-login-method
Add login method
This commit is contained in:
@@ -44,5 +44,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script type="module" src="../ts/index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,14 @@ import Service from './services/index';
|
||||
import View from './views/index';
|
||||
|
||||
export default class App {
|
||||
private controller: Controller;
|
||||
private _controller: Controller;
|
||||
|
||||
constructor() {
|
||||
this.controller = new Controller(new Service(), new View());
|
||||
this._controller = new Controller(new Service(), new View());
|
||||
}
|
||||
|
||||
start() {
|
||||
this.controller.registerController.init();
|
||||
this._controller.registerController.init();
|
||||
this._controller.loginController.init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,3 +26,9 @@ export const REGEX = {
|
||||
EMAIL:
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||
};
|
||||
|
||||
export const URL = {
|
||||
LOGIN: 'login',
|
||||
REGISTER: 'register',
|
||||
HOME: '',
|
||||
};
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import Service from 'services';
|
||||
import RegisterController from './registerController';
|
||||
import View from 'views';
|
||||
import LoginController from './loginController';
|
||||
|
||||
export default class Controller {
|
||||
public registerController: RegisterController;
|
||||
|
||||
public loginController: LoginController;
|
||||
|
||||
constructor(
|
||||
public service: Service,
|
||||
public view: View,
|
||||
) {
|
||||
this.registerController = new RegisterController(service, view);
|
||||
this.loginController = new LoginController(service, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import User from 'models/user';
|
||||
import Service from 'services';
|
||||
import View from 'views';
|
||||
import LoginView from 'views/loginView';
|
||||
|
||||
export default class LoginController {
|
||||
public loginView: LoginView | null = null;
|
||||
|
||||
constructor(
|
||||
public service: Service,
|
||||
public view: View,
|
||||
) {
|
||||
this.service = service;
|
||||
this.loginView = view.loginView;
|
||||
}
|
||||
|
||||
handlerLoginUser(email: string, password: string): Promise<boolean> {
|
||||
return this.service.userService.loginUser(email, password);
|
||||
}
|
||||
|
||||
handlerGetInfoUserLogin(): Promise<User | null> {
|
||||
return this.service.userService.getInfoUserLogin();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.loginView) {
|
||||
this.loginView.loadPage(this.handlerGetInfoUserLogin.bind(this));
|
||||
this.loginView.addHandlerForm(this.handlerLoginUser.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import View from 'views';
|
||||
import RegisterView from 'views/registerView';
|
||||
|
||||
export default class RegisterController {
|
||||
public registerView: RegisterView;
|
||||
public registerView: RegisterView | null = null;
|
||||
|
||||
constructor(
|
||||
public service: Service,
|
||||
public view: View,
|
||||
) {
|
||||
this.registerView = view.registerView;
|
||||
this.service = service;
|
||||
this.registerView = view.registerView;
|
||||
}
|
||||
|
||||
handlerCheckUserValid(email: string): Promise<boolean> {
|
||||
|
||||
@@ -18,3 +18,12 @@ export type TError = {
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export class CustomError extends Error {
|
||||
constructor(
|
||||
public title: string,
|
||||
message?: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const redirectToLoginPage = (): void => {
|
||||
window.location.replace('/login');
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export const redirectToLoginPage = (): void => {
|
||||
window.location.replace('/login');
|
||||
};
|
||||
|
||||
export const getSubdirectoryURL = () => {
|
||||
const url = window.location.href;
|
||||
const parts = url.split('/'); // Results: ['http:', '', 'example.com', '']
|
||||
const subDirectory = parts[3]; // Get subdirectory url only
|
||||
const index = subDirectory.indexOf('?'); // Remove query behind subDirectory
|
||||
|
||||
if (index !== -1) {
|
||||
return subDirectory.substring(0, index);
|
||||
}
|
||||
|
||||
return subDirectory;
|
||||
};
|
||||
@@ -1,38 +1,38 @@
|
||||
import { createIdUser } from '../helpers/data';
|
||||
|
||||
export default class User {
|
||||
private readonly id: number;
|
||||
private readonly _id: number;
|
||||
|
||||
private email: string;
|
||||
private _email: string;
|
||||
|
||||
private password: string;
|
||||
private _password: string;
|
||||
|
||||
private accessToken: string;
|
||||
private _accessToken: string;
|
||||
|
||||
constructor(email: string, password: string, accessToken?: string) {
|
||||
this.id = createIdUser();
|
||||
this.email = email;
|
||||
this.password = password || '';
|
||||
this.accessToken = accessToken || '';
|
||||
this._id = createIdUser();
|
||||
this._email = email;
|
||||
this._password = password || '';
|
||||
this._accessToken = accessToken || '';
|
||||
}
|
||||
|
||||
get getPassword() {
|
||||
return this.password;
|
||||
get password() {
|
||||
return this._password;
|
||||
}
|
||||
|
||||
set setAccessToken(accessToken: string) {
|
||||
this.accessToken = accessToken;
|
||||
set accessToken(accessToken: string) {
|
||||
this._accessToken = accessToken;
|
||||
}
|
||||
|
||||
get getAccessToken() {
|
||||
return this.accessToken;
|
||||
get accessToken() {
|
||||
return this._accessToken;
|
||||
}
|
||||
|
||||
get getEmail() {
|
||||
return this.email;
|
||||
get email() {
|
||||
return this._email;
|
||||
}
|
||||
|
||||
get getId() {
|
||||
return this.id;
|
||||
get id() {
|
||||
return this._id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ import {
|
||||
import { DATABASE_URL } from '../constants/config';
|
||||
|
||||
class FirebaseService {
|
||||
private app: FirebaseApp;
|
||||
private _app: FirebaseApp;
|
||||
|
||||
private db: Database;
|
||||
private _db: Database;
|
||||
|
||||
constructor() {
|
||||
const firebaseConfig = {
|
||||
databaseURL: DATABASE_URL,
|
||||
};
|
||||
this.app = initializeApp(firebaseConfig);
|
||||
this.db = getDatabase(this.app);
|
||||
this._app = initializeApp(firebaseConfig);
|
||||
this._db = getDatabase(this._app);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,25 +31,25 @@ class FirebaseService {
|
||||
* @returns {Promise} Return the resolves when write to database completed
|
||||
*/
|
||||
save(data: object, path: string): Promise<void> {
|
||||
return set(ref(this.db, path), data);
|
||||
return set(ref(this._db, path), data);
|
||||
}
|
||||
|
||||
delete(id: string, path: string): Promise<void> {
|
||||
return remove(ref(this.db, path + id));
|
||||
return remove(ref(this._db, path + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect to database
|
||||
*/
|
||||
disconnect(): void {
|
||||
goOffline(this.db);
|
||||
goOffline(this._db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect to database
|
||||
*/
|
||||
reconnect(): void {
|
||||
goOnline(this.db);
|
||||
goOnline(this._db);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,7 @@ class FirebaseService {
|
||||
): Promise<object | null> {
|
||||
return new Promise((resolve) => {
|
||||
onValue(
|
||||
ref(this.db, path),
|
||||
ref(this._db, path),
|
||||
(snapshot) => {
|
||||
let id: string | null = null;
|
||||
let data: object | null = null;
|
||||
@@ -104,7 +104,7 @@ class FirebaseService {
|
||||
getDataFromId(id: string, path: string): Promise<object> {
|
||||
return new Promise((resolve) => {
|
||||
onValue(
|
||||
ref(this.db, path + id),
|
||||
ref(this._db, path + id),
|
||||
(snapshot) => {
|
||||
resolve(snapshot.val());
|
||||
},
|
||||
@@ -118,7 +118,7 @@ class FirebaseService {
|
||||
getAllDataFromPath(path: string): Promise<object[]> {
|
||||
return new Promise((resolve) => {
|
||||
onValue(
|
||||
ref(this.db, path),
|
||||
ref(this._db, path),
|
||||
(snapshot) => {
|
||||
const listData: object[] = [];
|
||||
|
||||
@@ -149,7 +149,7 @@ class FirebaseService {
|
||||
): Promise<object[]> {
|
||||
return new Promise((resolve) => {
|
||||
onValue(
|
||||
ref(this.db, path),
|
||||
ref(this._db, path),
|
||||
(snapshot) => {
|
||||
let id: string;
|
||||
let data: object;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
class LocalStorageService {
|
||||
private localStorage: Storage;
|
||||
private _localStorage: Storage;
|
||||
|
||||
constructor() {
|
||||
this.localStorage = localStorage;
|
||||
this._localStorage = localStorage;
|
||||
}
|
||||
|
||||
add(key: string, value: string): void {
|
||||
this.localStorage.setItem(key, value);
|
||||
this._localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
get(key: string): string | null {
|
||||
return this.localStorage.getItem(key);
|
||||
return this._localStorage.getItem(key);
|
||||
}
|
||||
|
||||
remove(key: string): void {
|
||||
this.localStorage.removeItem(key);
|
||||
this._localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.localStorage.clear();
|
||||
this._localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class UserService {
|
||||
const user = await this.getUserByEmail(email);
|
||||
|
||||
// Check password
|
||||
if (user && user.getPassword === password) {
|
||||
if (user && user.password === password) {
|
||||
// Create token for user
|
||||
await this.createTokenUser(email);
|
||||
|
||||
@@ -71,14 +71,14 @@ export default class UserService {
|
||||
const newUserData = user;
|
||||
|
||||
// Add token to user object
|
||||
newUserData.setAccessToken = createToken();
|
||||
newUserData.accessToken = createToken();
|
||||
|
||||
this._commonService.save(newUserData);
|
||||
|
||||
// Add access token to local storage
|
||||
LocalStorageService.add(
|
||||
LOCAL_STORAGE.ACCESS_TOKEN,
|
||||
newUserData.getAccessToken,
|
||||
newUserData.accessToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { getSubdirectoryURL } from 'helpers/url';
|
||||
import LoginView from './loginView';
|
||||
import RegisterView from './registerView';
|
||||
import { URL } from 'constants/config';
|
||||
|
||||
export default class View {
|
||||
public registerView: RegisterView;
|
||||
public registerView: RegisterView | null = null;
|
||||
|
||||
public loginView: LoginView | null = null;
|
||||
|
||||
constructor() {
|
||||
this.registerView = new RegisterView();
|
||||
switch (getSubdirectoryURL()) {
|
||||
case URL.LOGIN:
|
||||
this.loginView = new LoginView();
|
||||
break;
|
||||
case URL.REGISTER:
|
||||
this.registerView = new RegisterView();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import AuthenticationView from './authenticationView';
|
||||
import { TypeToast, BTN_CONTENT } from '../constants/config';
|
||||
import User from 'models/user';
|
||||
import { DEFAULT_TITLE_ERROR_TOAST } from 'constants/messages/dialog';
|
||||
import { CustomError, TError } from 'global/types';
|
||||
import { redirectToLoginPage } from 'helpers/url';
|
||||
import { ERROR_CREDENTIAL } from 'constants/messages/form';
|
||||
|
||||
export default class LoginView extends AuthenticationView {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.initToast();
|
||||
this.initLoader();
|
||||
this.handleEventToast();
|
||||
}
|
||||
|
||||
async loadPage(getInfoUserLogin: () => Promise<User | null>) {
|
||||
this.toggleLoaderSpinner();
|
||||
const user = await getInfoUserLogin();
|
||||
if (user) {
|
||||
window.location.replace('/');
|
||||
}
|
||||
this.toggleLoaderSpinner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement error toast in site
|
||||
* @param {string} content The content will show in error toast
|
||||
*/
|
||||
initErrorToast(error: TError | string): void {
|
||||
const title =
|
||||
typeof error === 'object' && error.title
|
||||
? error.title
|
||||
: DEFAULT_TITLE_ERROR_TOAST;
|
||||
|
||||
const content =
|
||||
typeof error === 'object' && error.message
|
||||
? error.message
|
||||
: (error as string);
|
||||
|
||||
this.initToastContent(TypeToast.error, title, content, BTN_CONTENT.OK);
|
||||
|
||||
if (this.toastDialog && this.toastBtn) {
|
||||
// Show toast
|
||||
this.toastDialog.showModal();
|
||||
|
||||
// Remove event for toast button
|
||||
this.toastBtn.removeEventListener('click', redirectToLoginPage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener for form input
|
||||
* @param {Function} validateUser The function need to be set event
|
||||
*/
|
||||
addHandlerForm(
|
||||
loginUser: (email: string, password: string) => Promise<boolean>,
|
||||
) {
|
||||
if (this.formEl)
|
||||
this.formEl.addEventListener('submit', (e: Event) => {
|
||||
e.preventDefault();
|
||||
this.clearErrorMessage();
|
||||
this.submitForm(loginUser, e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The action when submit form
|
||||
* @param {Function} loginUser The function need to be set event
|
||||
* * @param {event} event The event target
|
||||
*/
|
||||
async submitForm(
|
||||
loginUser: (email: string, password: string) => Promise<boolean>,
|
||||
event: Event,
|
||||
) {
|
||||
try {
|
||||
// Load spinner
|
||||
this.toggleLoaderSpinner();
|
||||
// Get data from form
|
||||
const userInput = this.validateForm(event);
|
||||
if (userInput) {
|
||||
// Check user exist
|
||||
const results = await loginUser(userInput.email, userInput.password);
|
||||
if (results) {
|
||||
window.location.replace('/');
|
||||
return;
|
||||
}
|
||||
throw new CustomError(ERROR_CREDENTIAL.title, ERROR_CREDENTIAL.message);
|
||||
}
|
||||
} catch (error) {
|
||||
// Show toast error
|
||||
this.initErrorToast(error as string | TError);
|
||||
}
|
||||
// Close spinner
|
||||
this.toggleLoaderSpinner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from user input
|
||||
* @returns {Object || null} Return object or null
|
||||
*/
|
||||
validateForm(event: Event): User | null {
|
||||
if (event.target) {
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
// Validate user input
|
||||
this.listError = []; // Reset list error
|
||||
const emailValid = this.validateEmail(email);
|
||||
const passwordValid = this.validatePassword(password);
|
||||
|
||||
// Show error style
|
||||
if (this.emailEl && this.inputPasswordEl) {
|
||||
this.emailEl.classList.toggle('error-input', !emailValid);
|
||||
this.inputPasswordEl.classList.toggle('error-input', !passwordValid);
|
||||
if (emailValid && passwordValid) {
|
||||
return new User(email, password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.showError(this.listError);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TypeToast, BTN_CONTENT } from '../constants/config';
|
||||
import AuthenticationView from './authenticationView';
|
||||
import User from '../models/user';
|
||||
import { redirectToLoginPage } from '../helpers/redirect';
|
||||
import { redirectToLoginPage } from '../helpers/url';
|
||||
import {
|
||||
DEFAULT_MESSAGE,
|
||||
DEFAULT_TITLE_ERROR_TOAST,
|
||||
@@ -148,7 +148,7 @@ export default class RegisterView extends AuthenticationView {
|
||||
// Save user
|
||||
if (user) {
|
||||
// Check user exist
|
||||
const userExist = await checkExistUser(user.getEmail);
|
||||
const userExist = await checkExistUser(user.email);
|
||||
if (userExist) {
|
||||
throw Error(USER_EXIST_ERROR);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user