GithubHelp home page GithubHelp logo

amigoscode / full-stack-professional Goto Github PK

View Code? Open in Web Editor NEW
402.0 402.0 177.0 8.15 MB

Home Page: https://amigoscode.com/courses/full-stack-professional

Java 58.04% HTML 5.59% JavaScript 22.27% CSS 0.01% Shell 0.16% Dockerfile 0.09% TypeScript 13.31% SCSS 0.54%

full-stack-professional's People

Contributors

ali-alibou avatar ali-bouali avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

full-stack-professional's Issues

JWT Token

Well, I'm not positive about issuing JWT to a user who has been just signed up regardless of error:

To my mind we need to get a token and only then submit a profile to our database

 public ResponseEntity<?> registerCustomer(
            @Valid @RequestBody CustomerDto customerDto
    ) {
        var token = jwtUtil.issueToken(customerDto.email(), "ROLE_USER");
        
        customerService.addCustomer(customerDto);

        return ResponseEntity.ok()
                             .header(HttpHeaders.AUTHORIZATION, token)
                             .build();
    }

then I suggest add a couple of extra lines into your application.yml file

jwt:
  token:
    expires:
      minutes: 60
  secret:
    key: placewhateverkeyyoulikebutmakesureitislongenough_placewhateverkeyyoulikebutmakesureitislongenough_placewhateverkeyyoulikebutmakesureitislongenough_

and here is my jwtUtil.java

@Service
public class JwtUtil {

    private final Environment env;

    public JwtUtil(Environment env) {
        this.env = env;
    }

    public String issueToken(String subject) {
        return issueToken(subject, Map.of());
    }

    public String issueToken(String subject, String ...scopes) {
        return issueToken(subject, Map.of("scopes", scopes));
    }

    public String issueToken(
            String subject,
            Map<String, Object> claims
    ) {
        var currentTime = LocalDateTime.now();
        var expiredWithinMinutes = Integer.parseInt(Objects.requireNonNull(env.getProperty("jwt.token.expires.minutes")));

        return Jwts
                .builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()))
                .setExpiration(Date.from(currentTime.plusMinutes(expiredWithinMinutes).atZone(ZoneId.systemDefault()).toInstant()))
                .signWith(getSigningKey(), SignatureAlgorithm.HS512)
                .compact();
    }

    private Key getSigningKey() {
        var secretInBytes = Objects.requireNonNull(env.getProperty("jwt.secret.key")).getBytes();

        byte[] bytesEncoded = Base64.getEncoder().encode(secretInBytes);
        return Keys.hmacShaKeyFor(bytesEncoded);
    }

}

I would also recommend installing Spring CLI, then creating a keystore like :
keytool -genkeypair -alias myKeyAlias -keyalg RSA \ -dname "CN=Local,OU=Local,O=My laptop,L=Hyderabad,S=Telangana,C=India" \ -keypass keyPassword -keystore server.jks -storepass storePassword

then encrypt our secret and use {cipher} instead.... but why bother?

Implement the Access Guard and Secure the Customers Route

Here would be a tricky one.

Open your guard directory in a terminal window, and enter the following:
ng generate guard Access

In the app.module.ts file, in the bottom, paste the code:

export class AppModule {
    constructor(private injector: Injector) {
        AppInjector = this.injector;
    }
}

Right after all imports and before @NgModule paste the piece:
export let AppInjector: Injector;

you would also need to import Injector
import { Injector, NgModule } from "@angular/core";

I leave my complete authentication.service.ts,

import { Inject, Injectable, Injector } from "@angular/core";

import { AuthenticationRequest } from "../../models/authentication-request";
import { AuthenticationResponse } from "../../models/authentication-response";
import { Observable } from "rxjs";
import { HttpClient } from "@angular/common/http";
import { JwtHelperService } from "@auth0/angular-jwt";
import { Router } from "@angular/router";

@Injectable({
    providedIn: "root"
})
export class AuthenticationService {

    constructor(
        private http: HttpClient,
        private router: Router,
    ) {
    }

    authenticate(credentialsForLogin: AuthenticationRequest): Observable<AuthenticationResponse> {
        return this.http.post<AuthenticationResponse>(
            "http://localhost:8080/api/v1/auth/login",
            credentialsForLogin
        );
    }

    setSession(authenticationResponse: AuthenticationResponse): void {
        localStorage.setItem("userId", `${authenticationResponse.customerDto.id}`);
        localStorage.setItem("token", authenticationResponse.token);
    }

    isSessionValid(): boolean {
        const userId: number = Number(localStorage.getItem("userId"));
        const token: string = localStorage.getItem("token");

        if (this.isLocalStorageInfringed(userId, token) || this.isTokenExpired(token)) {
            this.logout();
            return false;
        }
        return true;
    }

    private logout(): void {
        localStorage.clear();
        this.router.navigate(["login"])
    }

    private isTokenExpired(token: string): boolean {
        return new JwtHelperService().isTokenExpired(token);
    }

    private isLocalStorageInfringed(userId: number, token: string): boolean {
        return !userId || !token;
    }
}

in login.component.ts insert in constructor

if (authenticationService.isSessionValid()) {
            router.navigate(["customer"]);
        }

the login function in the same file has setSeesion method you need to place

next: authenticationResponse => {
                    this.authenticationService.setSession(authenticationResponse);
                    this.router.navigate(["customer"]);
                },

Now the Guard itself.... I just don't know how to create an instance in the method... It complains on httpClient, then router.... so I've found a solution relying on injection

import { CanActivateFn } from "@angular/router";
import { AuthenticationService } from "../authentication/authentication.service";
import { AppInjector } from "../../app.module";

export const accessGuard: CanActivateFn = (route, state) => {
    return AppInjector.get(AuthenticationService).isSessionValid()
};

Suggestion to add a license file to the repository

Hello! I came across your code and I was wondering if you have considered adding a license file to your repository. A license file is a way of letting others know how they can use or modify your code legally. Without a license file, your code is technically not open source and no one can use it without your permission. This might limit the potential impact and reach of your project. GitHub recommends that you include a license in your repository to let others know how can your code be used and built upon (Source: Licensing a repository). If you are interested in making your project open source, you can check out Choosing a license to learn more about the different types of licenses and their benefits.

To add a license file to your repository, you can follow the steps in this guide: Adding a license to a repository. You can also read this article for more information on open source licensing: Open Source Licensing Guide.

Configuring Jib plugin - the issue with mvn clean package

There is a couple of things to be aware of:

  1. you need to use ${project.organization.name}
  2. It won't build and push an image if you don't specify the executions
<executions>
    <execution>
        <phase>package</phase>
        <goals>
            <goal>build</goal>
        </goals>
    </execution>
</executions>

The complete plugin part:

<plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <from>
                        <image>eclipse-temurin:20-jre</image>
                        <platforms>
                            <platform>
                                <architecture>arm64</architecture>
                                <os>linux</os>
                            </platform>
                            <platform>
                                <architecture>amd64</architecture>
                                <os>linux</os>
                            </platform>
                        </platforms>
                    </from>
                    <to>
                        <image>docker.io/${project.organization.name}/${project.artifactId}:${project.version}</image>
                        <tags>
                            <tag>latest</tag>
                        </tags>
                    </to>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

Now, you're good to go with mvn clean package

disable attribute in Button is no longer valid

According to the Chakra UI's docs there is no disabled attribute in the Button. Its substitute is isDisabled

I would also suggest destructuring formik and extracting the variable of dirty

({isValid, isSubmitting, dirty}) =>

<Button type="submit" isDisabled={ !dirty || !isValid || isSubmitting }>Submit</Button>

The button would be disabled once the page is loaded.

JdbcTemplate, updateCustomer and updateCustomerById

Both methods shown in the first part of the course are verbose and consist of the unnecessary logic.

Whenever we work with an API, we have a documentation in which one can find endpoints and how payload should look like

In case of a PUT request there is no need in updating one of the fields since PUT would modifies the whole resource anyway.

What you do relates more to PATCH request. Whenever you just want to modify a part of the resource. Let's say, the name
{
"name": "john doe"
}

sed is BRE and -E flag (extended) in MacOS

The issue is... it won't work as shown in the lesson:

sed -i -E 's_(amigoscode/amigoscode-api:)([^"]*)_\1'123456789'_' Dockerrun.aws.json

if you want it to work with the -E flag (extended mode), place a pair of single quote between two flags.

sed -i '' -E 's_(amigoscode/amigoscode-api:)([^"]*)_\1'123456789'_' Dockerrun.aws.json

or you may not use the extended mode you therefore need to escape each braces:

sed -i s_\(hazartilirot/hazartilirot-api:\)\([^\"]*\)_\1'123456789'_' Dockerrun.aws.json

Add Password To Create New Customer (lesson)

Well, I don't know what he does behind the scene you won't get a list of customers once a user has been registered.

I've taken a screenshot to show the code in the lesson
Screenshot 2023-05-14 at 14 18 17

The issue is simple as this one. When a user signs up as we don't save a token.

https://github.com/amigoscode/full-stack-professional/blob/react-upload/frontend/react/src/components/shared/CreateCustomerForm.jsx

If you look at his last release there is a function onSuccess

onSubmit={(customer, {setSubmitting}) => {
                    setSubmitting(true);
                    saveCustomer(customer)
                        .then(res => {
                            console.log(res);
                            successNotification(
                                "Customer saved",
                                `${customer.name} was successfully saved`
                            )
                            onSuccess(res.headers["authorization"]);
                        }).catch(err => {
                            console.log(err);
                            errorNotification(
                                err.code,
                                err.response.data.message
                            )
                    }).finally(() => {
                         setSubmitting(false);
                    })
                }}

While typing the message I just unable to figure out the meaning of the snippet:
onSuccess(res.headers["authorization"]);

onSuccess is fetchCustomers() function which doesn't accept arguments. What's the meaning of the code I just don't know.

I did basically as following:

onSubmit={
                    (customer, { setSubmitting }) => {
                        setSubmitting(true);
                        registerCustomer(customer)
                            .then(res => {
                                notifyOnSuccess("Customer saved", `${customer.name} was saved`);
                                return login({
                                    username: customer.email,
                                    password: customer.password
                                })
                            }).then(res => location.reload())
                            .catch(err => notifyOnFailure(err.code, err.response.data.error))
                            .finally(() => setSubmitting(false))
                    }
                }

Don't forget to import the useAuth() to the JSX component
const { login } = useAuth();

In nutshell, we register a new user, then we run our login function, we pass username and password and then reload the page. I have no idea if it is implemented in a correct way - but it works!

Create Customer Service (Angular - Fetch All Customers)

Actually, the solution shown in the video is incorrect since we have protected the route /api/v1/customers

We need to add a token each time we request. There should be something like the following:

findAll(): Observable<CustomerDto[]> {
        return this.http.get<CustomerDto[]>(
            "http://localhost:8080/api/v1/customers", {
                headers: new HttpHeaders({
                    "Authorization": `Bearer ${localStorage.getItem("token")}`
                })
            }
        );
    }

Directives - conditions - Ngfor (Part 2) - missing code in the beginning

For no reason a piece of code has been cut out of the video. I figured out on my own how it should be written in order to get the result

<input type="text" [(ngModel)]="inputValue">
<button (click)="clickMe()">click</button>
<p>Your name is: {{inputValue}}</p>

<p *ngFor="let msg of msgList">{{ msg }}</p>
import { Component } from '@angular/core';

@Component({
  selector: 'app-my-first-component',
  templateUrl: './my-first.component.html',
  styleUrls: ['./my-first.component.scss']
})
export class MyFirstComponent {
  inputValue: string = '';
  msgList: string[] = [];

  clickMe = (): void => {
    this.msgList.push(this.inputValue);
    this.inputValue = '';
  }
}

POSTGRES_DB: customer in compose.yml

Basically, if you open the official docker postgres page there would be the subject's description.
You just need to add this line to your compose.yml file in order to create the default database customer
POSTGRES_DB: customer

version: "3.9"

services:
  db:
    container_name: postgres
    image: postgres:latest
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: root
      POSTGRES_DB: customer
      PGDATA: /data/postgres
    volumes:
      - db:/data/postgres
    ports:
      - 5432:5432
    networks:
      - db
    restart: unless-stopped

networks:
  db:
    driver: bridge
volumes:
  db:

selectCustomerById Test

That's really strange! We've got a block @beforeeach setup.... why don't we place the piece of code into it?

var customer = new Customer(
                FAKER.name().fullName(),
                FAKER.internet().safeEmailAddress(),
                new Random().nextInt(18, 99)
        );

customerJDBCDataAccessServiceUnitTest.insertCustomer(customer);

Why does he use the email address to find the customer we've just inserted? He basically relies on a method he hasn't tested yet. It's a shame!

If the previous test is passed we mainly need to rely on it instead.

    @Test
    void selectCustomerById() {
        var customer1 = customerJDBCDataAccessServiceUnitTest.selectAllCustomers()
                                                             .stream()
                                                             .findAny()
                                                             .orElseThrow();
        var customer2 = customerJDBCDataAccessServiceUnitTest.selectCustomerById(customer1.getId())
                                                             .orElseThrow();
        assertThat(customer1.getId()).isEqualTo(customer2.getId());

    }

We don't care what email our inserted user has.... All we care about is if there is a user at all. We get the user and then we test our method looking the user by its ID. Here is the code:

@BeforeEach
    void setUp() {
        customerJDBCDataAccessServiceUnitTest = new CustomerJDBCDataAccessService(getJdbcTemplate());

        var customer = new Customer(
                FAKER.name().fullName(),
                FAKER.internet().safeEmailAddress(),
                new Random().nextInt(18, 99)
        );

        customerJDBCDataAccessServiceUnitTest.insertCustomer(customer);
    }

    @Test
    void selectAllCustomers() {

        var customers = customerJDBCDataAccessServiceUnitTest.selectAllCustomers();

        assertThat(customers).isNotEmpty();
    }

    @Test
    void selectCustomerById() {
        var customer1 = customerJDBCDataAccessServiceUnitTest.selectAllCustomers()
                                                             .stream()
                                                             .findAny()
                                                             .orElseThrow();
        var customer2 = customerJDBCDataAccessServiceUnitTest.selectCustomerById(customer1.getId())
                                                             .orElseThrow();
        assertThat(customer1.getId()).isEqualTo(customer2.getId());

    }

Redirect to Dashboard if logged in

Lesson Redirect to Dashboard if logged in, timeline 02:00

Oh my gooooood. The more I watch the less I like.... Mama Samba is soooooooo impatient and unreasonable. I have just implemented the feature of fetching a user by id. Instead of using the null in the initial state in AuthContext.jsx - I used a blank object as a kind of a guest with ROLE_GUEST. I decided to go this way so that Sidebar.jsx is loaded properly.

AuthContext.jsx

import { createContext, useContext, useEffect, useState } from "react";
import { executeLogin, getCustomerById } from "../../services/client.js";
import jwtDecode from "jwt-decode";
import { redirect } from "react-router-dom";

const AuthContext = createContext({});

const AuthProvider = ({ children }) => {

    let defaultCustomer = {
        name: "Viewer",
        email: "",
        age: undefined,
        gender: "",
        roles: ["ROLE_GUEST"]
    };

    const [customer, setCustomer] = useState(defaultCustomer);

    useEffect(() => {
            getCustomer().then(data => setCustomer({ ...data }))
    }, []);


    const getCustomer = async () => {
        const token = localStorage.getItem("access_token");
        const id = localStorage.getItem("customer_id");

        if (!token || !id) {
            return logout();
        }

        const { data } = await getCustomerById(id)

        const decodedJwt = jwtDecode(token);

        if (!data.email === decodedJwt.sub) {
            logout();
        }
         return data;
    }

    const login = async submitEmailPassword => new Promise(
        (resolve, reject) => executeLogin(submitEmailPassword)
            .then(res => {
                const jwt = res.headers["authorization"];
                localStorage.setItem("access_token", jwt);
                localStorage.setItem("customer_id", res.data.customerDto.id);

                getCustomer().then(fetched => {
                    setCustomer({ ...fetched })
                    resolve(res);
                });
            }).catch(err => {
                reject(err);
            })
    );

    const logout = () => {
        localStorage.removeItem("access_token");
        localStorage.removeItem("customer_id");
        setCustomer(defaultCustomer);
        redirect("/")
    };

    const isCustomerAuthenticated = () => {

        const token = localStorage.getItem("access_token");
        const id = localStorage.getItem("customer_id");

        if (!token || !id) {
            logout();
            return false
        }

        const { exp } = jwtDecode(token);

        if (Date.now() > exp * 1000) {
            logout();
            return false;
        }

        return true;
    };

    return (
        <AuthContext.Provider value={{
            customer,
            login,
            logout,
            isCustomerAuthenticated
        }}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => useContext(AuthContext);

export default AuthProvider;
export const getCustomerById = async id => {
    try {
        return await axios.get(
            `${import.meta.env.VITE_API_BASE_URL}/api/v1/customers/${id}`,
            getTokenFromLocalStorage()
        );
    } catch (e) {
        throw e;
    }
}

Each request I take the user's id from the localStorage, fetch the user from the database by the id and place the user into the state. Before that I retrieve the user's email from JWT and compare it with the customer's email - if they are the same - ok. If any of localStorage values have been removed from the client's side (jwt or user id) I redirect the user to the login page and delete any data in their localStorage.

What he does in the Login component is he checks if the user is null (state). The difference is he doesn't fetch the user from the database (there is no async request) and it seems to have worked perfectly well. In my case, by the time it checks the user it is yet GIMMICK (a guest! or it would've been null if I have done the same as he does in the video). Anyway, what we actually need to check is not the user's state, but if the user is authenticated:

Login.jsx

const { isCustomerAuthenticated } = useAuth();
    const navigate = useNavigate();

    useEffect(() => {
        if (isCustomerAuthenticated()) {
            navigate("dashboard")
        }
    }, [])

or, an alternative way....:

const navigate = useNavigate();

    useEffect(() => {
        const token = localStorage.getItem("access_token");
        const id = localStorage.getItem("customer_id");

        if (token && id) {
            navigate("dashboard")
        }
    }, [])

if you're going to implement the same, there are extra two pieces of code:

onSubmit={(values, { setSubmitting }) => {
                setSubmitting(true);
                login(values)
                    .then(res => {
                        navigate("dashboard");
                    }).catch(err => notifyOnFailure(err.code, err.response?.data.message)
                ).finally(() => setSubmitting(false));

            }}
 onSubmit={
                    (customer, { setSubmitting }) => {
                        setSubmitting(true);
                        registerCustomer(customer)
                            .then(res => {
                                notifyOnSuccess("Customer saved", `${customer.name} was saved`);
                                return login({
                                    username: customer.email,
                                    password: customer.password
                                })
                            }).then(res => location.reload())
                            .catch(err => notifyOnFailure(err.code, err.response.data.error))
                            .finally(() => setSubmitting(false))
                    }
                }

One is used when a user logs in to their account, another one is when a user signs up.

make your containers talk to each other (without docker-compose)

Okay, the issue I had personally (you most likely have the same) I didn't use his settings in docker-compose.yml file for database.
I actually had a container with many databases running so I decided to stick to my existent database.

docker run -d --name YOUR_DOCKER_CONTAINER_NAME-api --rm -p 8080:8080 --network DATABASE_NETWORK YOUR_DOCKER_ACCOUNT/YOUR_CONTAINER_NAME-api --spring.datasource.url=jdbc:postgresql://THING_WE_ARE_LOOKING_FOR:5432/customer

-d - detached mode

--name YOUR_DOCKER_CONTAINER_NAME-api - in your case it might be different.

--network DATABASE_NETWORK -

if you have your database container running, just use the command:

docker network ls

it will list all of your networks. You're interested in the Name of your database ID's container. In my case it's hibernate-starter_default Basically, we need to connect the container to this bridge.

YOUR_DOCKER_ACCOUNT/YOUR_CONTAINER_NAME-api - see the info in the docker repository it is a new container you have pushed.

--spring.datasource.url=jdbc:postgresql://THING_WE_ARE_LOOKING_FOR:5432/customer - it's really tricky one.

docker inspect ID_DATABASE_CONTAINER -f "{{json .NetworkSettings.Networks }}"

It will give you something like this:

{"hibernate-starter_default":{"IPAMConfig":null,"Links":null,"Aliases":["hibernate-starter-db-1","db","8543027d7b75"],"NetworkID":"4ad36a6716d855dfc93e1192acfae8024fa043b5118602559b5d6f130ed9994d","EndpointID":"7d4f4d8c66f7213a513774952a3ea55c790d0715a705eacd5fc01e0ede6e45b5","Gateway":"172.18.0.1","IPAddress":"172.18.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:12:00:02","DriverOpts":null}}

You may use any Aliases name, like hibernate-starter-db-1 or db or 8543027d7b75 so in my case, I use

--spring.datasource.url=jdbc:postgresql://db:5432/customer

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.