Giter Site home page Giter Site logo

joe-jngigi / joe_jngigi_beta Goto Github PK

View Code? Open in Web Editor NEW
0.0 1.0 0.0 7.17 MB

This is a flagship and a very important project, I has had many iterations and UI changes, having worked on it since I started working with next.js in 2023

Home Page: https://joe-jngigi.vercel.app/

JavaScript 0.21% TypeScript 96.47% MDX 0.08% CSS 3.24%
mdx mongodb next-auth nextjs14 shadcn-ui tailwindcss typescript astradb langchain-typescript openai react-markdown upstash-redis

joe_jngigi_beta's Introduction

NextJS Development

Pipet! Important

This is a Next.js project bootstrapped with create-next-app.

Code Snippets

<p className="date">
   {new Date(`${blogPost.datePublished}`).toLocaleDateString(
     'en-us',
     {
       year: 'numeric',
       month: 'short',
       day: 'numeric',
     }
   )}
</p>

Random Modal Component Code

<div className='md:hidden overflow-y-hidden bg-slate-500 bg-transparent backdrop-blur-lg fixed top-0 z-50 h-[100%] w-[100%]'>
    <div className='absolute top-5 right-5 text-slate-50 '>
      <button className='text-2xl border-2 border-slate-50 rounded-full p-2' onClick={() =>{
        document.body.style.overflow = 'unset'}}>
        <MdClose/>
      </button>
    </div>
    <div className='absolute top-0 bg-blend-luminosity bg-black  bg-opacity-90  w-[70%]  h-[100vh] p-2 md:p-5 text-white'>
      Yeap
    </div>
</div>

Getting Started

The layout entry point for the nextjs components is the main entry point of our application and all the components are wrapped within it as its children. As a result, any code that is written will be displayed on every route page that you get to create; like a </ Header> and a </ Navbar>. This means that the component accepts a children prop.

In NextJs we have the built-in pages that handle the different layouts. The can include:

  • layout.tsx ( *js, *jsx)
  • page.tsx ( *js, *jsx)
  • error.tsx ( *js, *jsx)
  • not-found.tsx ( *js, *jsx)
  • loading.tsx ( *js, *jsx)

Anytime you write this, next framework will recognize this page according to what it is defined for.

Highlight: The spread operator

The spread operator (...) in JavaScript is used to "spread" the contents of an iterable (like an array or an object) into another array or object. Let's break down how this works using your marvelMovies array:

const marvelMovies = [
    "Iron Man (2008)",
    "The Incredible Hulk (2008)",
    // ... (other movies)
];

// Using the spread operator to spread the data from each item in the marvelMovies array
<Postcard {...eachData} />;

Compare the above with this:

const marvelMovies = [
    {1: "Iron Man (2008)"},
    {2: "The Incredible Hulk (2008)"},
    // ... (other movies)
];

// Using the spread operator to spread the data from each item in the marvelMovies array
<Postcard {...eachData} />;

eachData represents one of the items from the marvelMovies array. eachData is a string, such as "Iron Man (2008) or "The Incredible Hulk (2008)" When you use the spread operator with {...eachData}, you're telling JavaScript to treat the string as an iterable and spread its characters into separate properties in an object. For this we get this for each of the string.

{
  "0": "B",
  "1": "l",
  "2": "a",
  "3": "c",
  "4": "k",
  "5": " ",
  "6": "W",
  "7": "i",
  "8": "d",
  "9": "o",
  "10": "w",
  "11": " ",
  "12": "(",
  "13": "2",
  "14": "0",
  "15": "2",
  "16": "1",
  "17": ")"
}

For the latter, the console.log(eachData) will be as shown. Note that the last one is a string,

{ '1': 'Iron Man (2008)' }
{ '2': 'The Incredible Hulk (2008)' }
{ '3': 'Iron Man 2 (2010)' }
{ '4': 'Thor (2011)' }
{ name: 'Spider-Man: Far From Home (2019)' }
Black Widow (2021)

If you console.log({...eachData}) you get the following

{ '1': 'Iron Man (2008)' }
{ '2': 'The Incredible Hulk (2008)' }
{ '3': 'Iron Man 2 (2010)' }
{ '4': 'Thor (2011)' }
{ name: 'Spider-Man: Far From Home (2019)' }
{
  '0': 'B',
  '1': 'l',
  '2': 'a',
  '3': 'c',
  '4': 'k',
  '5': ' ',
  '6': 'W',
  '7': 'i',
  '8': 'd',
  '9': 'o',
  '10': 'w',
  '11': ' ',
  '12': '(',
  '13': '2',
  '14': '0',
  '15': '2',
  '16': '1',
  '17': ')'
}

Take away

When you use the spread operator with {...eachData}, you're telling JavaScript to treat the string as an iterable and spread its characters into separate properties in an object. Also, we note that in TS, for any data we describe, it is given any type, so we can describe the data as shown in this section of the code

FETCHED_DATA.map((eachData: { id: number; title: string; body: string }) => (
  <Postcard key={eachData.id} {...eachData} />
));

NB This type declaration ensures that when you use the eachData object in your code, it should adhere to this specific structure with the defined data types. For example, it prevents you from assigning a non-number value to the id property or a non-string value to the title property.

Check out the code structure in Learning Typescript

Mouse event

You note that when you are handling the button, and you want to perform an event, it will need to have a mouse event parameter passed in to do the e.preventDefault. If you hover on the onClick event used it the button, it will show the data type required for the button.

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) =>{
  e.preventDefault()
  console.log('deleted succesfully');
}

In a form submission context, e.preventDefault() can be used to prevent the default action of submitting the form, allowing you to perform custom actions like client-side validation or data manipulation before sending the data to the server.

States With TS

When using useState in react, you can pass in an object into as state to receive values, like shown

const [User, setUser] = useState(null)
setUser({
  name: username,
  sessionId: Math.random()
})

In Typescript, you will note that name has a Lint.

Error
Argument of type { name: string; sessionId: number; } is not assignable to parameter of type SetStateAction<null>. Object literal may only specify known properties, and name does not exist in type (prevState: null) => null.

Linting, is the process of analyzing your code to find potential issues, coding style violations, and other problematic patterns. You can identify this using ESLint, which is a popular open-source linting tool for JavaScript and TypeScript.

In simple terms, you are giving name and sessionId, but you specified that it can be null. To prevent the error, we will use generics

Generics in TypeScript are a powerful feature that allow you to write functions, classes, and types that can work with a variety of data types while maintaining type safety. They allow you to maintain type safety while still dealing with flexible types.

export type userType = {
    sessionId: number,
    name: string
}

const [User, setUser] = useState<userType | null>(null)
setUser({
  name: username,
  sessionId: Math.random()
})

Use case of generics

import React, { useState } from 'react';

// Define a generic ListProps
type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => JSX.Element;
};

// Generic List component
function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Example usage
type Person = {
  name: string;
  age: number;
};

function App() {
  const persons: Person[] = [
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 25 },
    // ...
  ];

  return (
    <div>
      <h1>Person List</h1>
      <List items={persons} renderItem={(person) => <div>{person.name} - {person.age}</div>} />
    </div>
  );
}

export default App;

In the above piece of code, you note that ListProps is a type defined, and it has generics. In the prop, items receives this array persons and then renderItem receives a function as a JSX element

const persons: Person[] = [
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 25 },
    // ...
];

renderItem={(person) => <div>{person.name} - {person.age}</div>}

Take away #2

If you omit (null) and just write useState<userType | null>(), TypeScript will still understand that the initial state value is null, as useState automatically infers the initial state value based on the type you provide in the generic parameter. However, including (null) can improve code readability and make the intent more obvious.

In a nutshell, generics allow you to define placeholders for types that will be specified when the code is used. This helps you write more flexible and reusable code that doesn't sacrifice type checking.

function identity<T>(value: T): T {
  return value;
}

const stringValue: string = identity("Hello, TypeScript!");
const numberValue: number = identity(42);

Check out the code at State with NextJS

Context API

The Context API is a feature in React, a JavaScript library for building user interfaces, that provides a way to manage and share state across components without having to pass props manually through every level of the component tree.

To create context, you want to start by importing createContext

Forms in HTML

Big Question
Does a form capture the data from the inputs automatically?

Absolutely Yes, a form captures data from the input fields automatically when it's submitted. However, in the provided code, there are a few modifications needed to correctly capture and handle the form data. Remember in this code, I am using typescript, so I will need to be able to handle data types for the data entered by the user

I really don't require this piece of code, but I learned it anyway

const formData = new FormData(e.currentTarget)
  const userData: TUserTypes = {
    user_email: '',
    user_password: '',
  }

  formData.forEach((value, key) =>{
    if (typeof value === 'string') {
      userData[key as keyof TUserTypes] = value;
    } else if (value instanceof File) {
          // Handle the File object, if needed
    }
      })

So basically this is how the form is supposed to be created

<form onSubmit={handleSubmit} action="AddProduct">
  <input required type="text" name="product_name" id="product_name" placeholder="Product Name"/>
  <input required type="text" name="product_price" id="product_price" placeholder="Product Price"/>
  <input required type="text" name="product_description" id="product_description" placeholder="Product Description"/>
  <input required type="text" name="product_size" id="product_size" placeholder="Product Size"/>
  <input required type="text" name="product_color" id="product_color" placeholder="Product Color"/>

  <button>Add Product</button>
</form>

Take note of the type and the name of the input. The type will describe the data that will be entered in the input, and then the name, will be responsible for the key for the data to be collected from the inputs. In order to collect the data, you will need to define a function to handle the form submission and capture the data from the input fields. You can note that on the <form onSubmit={handleSubmit}>{....}</form> we have defined a function called handleSubmit.

This is the code for responsible for capturing the data from the input fields

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) =>{
  e.preventDefault();

  const formData = new FormData(e.currentTarget)
  console.log(formData.get('product_name'));
  const productData: productsProps = {
    product_name: '',
    product_price: '',
    product_description: '',
    product_size: '',
    product_color: ''
  }
    formData.forEach((value, key) =>{
      if (typeof value === 'string') {
        productData[key as keyof productsProps] = value;
    } else if (value instanceof File) {
        // Handle the File object, if needed
    }
    })
    console.log(productData);
  }

So we have const formData = new FormData(e.currentTarget)

The FormData object is a built-in JavaScript object that is used to capture form data and send it as part of an HTTP request, typically in the context of AJAX requests or form submissions.

So new FormData(e.currentTarget) creates a new instance of the FormData object and initializes it with the data from the form element (e.currentTarget). This property of the event object refers to the DOM element that the event handler is currently attached to. In this case, it refers to the form element <form> that is being submitted.

I could basically use this piece of code to get individual data of an input console.log(formData.get('product_name'));

By passing the form element to FormData, you're essentially capturing all the input values, including text inputs, file inputs, checkboxes, and other form elements, as well as their associated names and values. This data is then stored in the formData object.

Creating Context

A context is a mechanism that allows components to share data without the need to pass props explicitly through every level of the component tree. It provides a way to manage and share state or other values across different components, even if they are not directly related in the component hierarchy. In simple terms it can be used to share data across the application. Using context, you can create a context provider component that wraps a portion of your component tree. This provider makes the specified data or functions available to any component.

How can one create a context? We start by first creating a context like shown below. And since I am using typescript, I will define a data structure initially like shown. You note that we first import create context like shown bellow,

import { createContext } from 'react';

type AuthContextType = {
  isAuthenticated: boolean;
  login: () => void;
  logout: () => void;
};

const initialAuthStatus: AuthContextType = {
    isAuthenticated: false,
    login: () => {},
    logout: () => {},
}


export const AppContext = createContext<AuthContextType>(initialAuthStatus);

What is initialAuthStatus?
It is called a default value, and is required to be passed in so that when you have forgotten to provide context, that it is used when there is no provider. That means that everything falls back to default value. Let me break down what is happening here.

createContext is a built-in react function used to provide context. AuthContextType is used in as the generics, and it is used to define the default data type of the context. initialAuthStatus This can be anything, and it used to define a default value as shown. I will be using a simpler example to see how we can use the context. Suppose we want to change the theme of our application to be maybe dark mode or light mode. You notice that when we pass in the value light, it will have a lint Argument of type 'string' is not assignable to parameter of type 'ThemeTypes'.

export const ThemeContext = createContext<ThemeTypes>('light')

Since ThemeTypes is describing a datatype, same case we will create a variable, with the same structure, that is why we pass it in as an object. An example is shown on how the data type works.

// An example is shown on how the data type works.
const ThemedType: ThemeTypes = {
    theme_value: 'light'
}
import { createContext } from 'react';

type ThemeTypes ={
  theme_value: 'light' | `dark`
}
// const ThemeContext = createContext('light') => for JS

export const ThemeContext = createContext<ThemeTypes>({
  theme_value: 'light'
})

Now we can import the exported ThemeContext to be available to our components. This happens to the main/parent component that carries all the other components. On importing, the ThemeContext will have three properties; provider, consumer and display name. Consumer is for components that consume those contexts; those that get the value of the provided context. Provider Provides the value to the components it wraps around. Provider takes all properties a component can take like value, key and children. Children are passed in implicitly

import React from 'react'
import Link from 'next/link'
import { ThemeContext } from './context/ThemeContext'

 const [themeValue, setThemeValue] = useState<'light' | 'dark' >(defaultThemeValue.themeValue) )

const ProductsLayout = ({children}: {children: React.ReactNode}) => {
  return (
    <div className='min-h-590'>
      <ThemeContext.Provider value = {{themeValue}}>
        // So all the children here have access to the state of theme value
      </ThemeContext.Provider>
    </div>
  )
}

So far, we realize that the state mutation is happening in the layout file (This is basically the component holding the children components). We would want to have context change in maybe a specific child component. What do I mean, If I have a </newProduct> component and want to mutate a state inside that, I will have to wrap it in <ThemeContext.Provider>

So what we do is that we can define a custom component in the context provider file

import { createContext, FC } from "react";
import { ThemeTypes } from "@/app/typescript/types/types";

export const defaultThemeValue: ThemeTypes = {
    themeValue: 'light'
}

export const ThemeContext = createContext<ThemeTypes>(defaultThemeValue)


export const ThemeContextProvider:FC<ThemeTypes> = () => {

  return  <ThemeContext></ThemeContext>

}

TProducts: An array of productsProps type. It's intended to store an array of products, each represented by a productsProps object. So this means that TProducts will have the following structure.

Const TProducts = [
  {
    "product_id": 1,
    "product_name": "Smartphone X",
    "product_price": 799,
    "product_description": "High-end mobile device",
    "product_size": "5.8 inches",
    "product_color": "Black"
  },
  {
    "product_id": 2,
    "product_name": "Laptop Pro",
    "product_price": 1299,
    "product_description": "Powerful laptop for professionals",
    "product_size": "13.3 inches",
    "product_color": "Silver"
  },]

TAddProduct: A function that takes a single argument of type productsProps and returns void. This function is intended to add a new product to the array stored in TProducts.

Take away 3

Can I safely say a type/interface is used to describe a data structure of what is intended?
Yes, that's a very accurate description of the purpose of types and interfaces in programming, particularly in languages like TypeScript. Both types and interfaces are used to define the structure and shape of data. They help ensure that data adheres to a specific structure and type.

In TypeScript, using the syntax 'light' | 'dark' as the type of property indicates that the property can only take one of those two specific string values. This enforces type safety and helps prevent unintended values from being assigned to the property.

Pagination in React

In this code, I am making a pagination, and as provided in the code, we will have different elements we use

const lastIndex = CurrentPage * recordsPerPage, on this piece of code, it is going to indicate, the last number for of the current count of data. Simply that means that if we have the first page with 25 records, so last index will hold number 25 and if we have the second records page, it will have lastIndex => 2 * 25 = 50

const firstIndex = lastIndex - recordsPerPage on this it means that the value for the firstIndex is the first number on a new page. So this is; I have 25 records per page, so the new number will be firstIndex => 50 - 25 = 25

Function components in TS

The use of FC after MyFunctionalComponent is a type annotation in TypeScript. It explicitly specifies that MyFunctionalComponent is a functional component and gives it the type FC, which is typically imported from the React library. FC stands for Functional Component, and it's a predefined type in TypeScript that helps you define the prop types for functional components. It ensures type safety.

import React, { FC } from 'react';

interface MyComponentProps {
  name: string;
  description: string;
}

const MyFunctionalComponent: FC<MyComponentProps> = (props) => {
  return (
    <div>
      <h1>Hello, {props.name}!</h1>
      <p>{props.description}</p>
    </div>
  );
};

In this updated code, MyComponentProps is an interface that defines the prop types for MyFunctionalComponent, and FC<MyComponentProps> specifies that this functional component expects props conforming to those types. This adds a layer of type safety and helps communicate the expected props to anyone working with the component.

const App = () => {
  // This will cause a TypeScript error because 'extraProp' is not in MyComponentProps
  return (
    <MyFunctionalComponent name="John" description="A description" extraProp="Extra" />
  );

When you attempt to pass an additional prop extraProp to the component, TypeScript will raise a type error because extraProp is not part of the expected prop types defined in MyComponentProps.

import React from 'react';

interface MyComponentProps {
  name: string;
  description: string;
}

const MyFunctionalComponent = (props: MyComponentProps) => {
  // TypeScript knows that `props` has the type `MyComponentProps`
  return (
    <div>
      <h1>Hello, {props.name}!</h1>
      <p>{props.description}</p>
    </div>
  );
};

Both approaches achieve the same result: they specify that props should conform to the MyComponentProps interface, and TypeScript enforces this type checking. You can choose the approach that you find more readable and maintainable for your codebase. Some developers prefer the FC type because it's a bit shorter and often used in React projects, while others prefer the explicit annotation for clarity.

Authentication Adding

In this section, I am learning more on Authentication using next.js. For this I will be applying a custom authentication with custom credentials. I have not used any of the authentication provider, and this is because I have used the clerk provider extensively in another project. This will be helping me sharpen some skills in that.

On this project, I will be using nextAuth It is a completely secured and flexible authentication library designed to sync with any OAuth service, with full support for passwordless signin. I will install bcryptjs, next-auth and mongoose; bcryptjs will be used in the encryption of the passwords and then mongoose will be used for the database, mongodb

In the next phase I will be creating the API routes to help me make data transfer to the database.

Creating APIs

We create APIs in the API folder, which is in the app folder. This only applies to NextJS. In the context of APIs (Application Programming Interfaces), POST and DELETE are HTTP methods used to perform different operations on resources.

We start by receiving the data from the request, which is returned as a promise. I mean how does this work?

This below is the code that is user to send the data to the database. So, we destructure each section

export const POST = async (req: Request) => {
    try {
        const user_data = await req.json();
        const { user_password, user_email, user_name } = user_data;

        // Your logic for handling the user data goes here
    } catch (error) {
        // Handle errors
    }
};
  • The function is marked as async, indicating that it will perform asynchronous operations.
  • The function takes a single parameter req, which is assumed to be Express.js Request object.
  • This line is using await to asynchronously wait for the JSON body of the incoming HTTP request (req) to be parsed. req.json() is a method provided by Express.js to parse the JSON body of a request.

Take away 4

Also, on this note, I wanted to check whether the email existed, so that the user can not create an account using the same email again. This, to my surprise, implemented the POST method and not the GET method. The choice between using a GET request and a POST request depends on the nature of the operation and the conventions of RESTful API design. In this case it meant that for the POST, Then I could affect the changes, and secondly, I only wanted the email not to be cached.

GET: It is considered idempotent, meaning that making the same request multiple times should have the same effect as making it once. GET requests are typically used for retrieving information and should not have side effects on the server. POST: It is not necessarily idempotent, and it is often used for operations that can cause a change in the server's state. In your case, it looks like the API is checking for the existence of a user based on their email, which could be considered a non-idempotent operation.

Take away 5

This is how we create APIs in JS. Note we have to install express

const express = require('express');
const app = express();
const port = 3000; // You can use any available port

// Define a route for the API
app.get('/api/greeting', (req, res) => {
    res.json({ message: 'Hello, this is your API!' });
});

// Start the server
app.listen(port, () => {
    console.log(`Server is running on http://localhost:${port}`);
});

What are asynchronous operations?

Asynchronous operations in programming refer to tasks that don't necessarily execute in a sequential or synchronous order. Instead of waiting for each operation to complete before moving on to the next one, asynchronous programming allows a program to continue executing other tasks while waiting for certain operations to finish. This is particularly useful for tasks that might take some time to complete, such as reading from a file, making a network request, or querying a database.

Connecting to Database

I am using Mongodb Atlas. We use this npm package. Remember this is a no SQL database. There are a various way to connect to a MongoDB database, I am familiar with using MongoDB Driver and Mongoose Client. Mongoose is better because it is robust. Below, I will provide the code used for connection to the mongoDB using Mongoose

npm isntall mongoose
import mongoose from 'mongoose';

const MONGO_URI = process.env.MONGODB_URI as string;

let IS_CONNECTED = false;

if (!MONGO_URI) {
  throw new Error('Please define the MONGO_URI environment variable');
}

const connectDB = async () => {
  if (IS_CONNECTED) {
    console.log('Already connected.');
    return mongoose.connection; // Return the connection object
  }

  try {
    await mongoose.connect(MONGO_URI);
    IS_CONNECTED = true;
    console.log('Connection Established');
    return mongoose.connection; // Return the connection object
  } catch (error) {
    console.error('Error when establishing a connection:', error);
    throw error; // Rethrow the error to be caught in the calling function
  }
};

export default connectDB;

The first line imports the Mongoose library, which is an ODM (Object Data Modeling) library for MongoDB and Node.js. It provides a way to interact with MongoDB using JavaScript or TypeScript. The line await mongoose.connect(MONGO_URI); is a crucial part of establishing a connection to a MongoDB database using Mongoose.

The mongoose.connect: This is a method provided by the Mongoose library to connect to a MongoDB database. It establishes a connection between a Node.js application and the specified MongoDB database through the MONGO_URI. It's worth noting that this line is typically called when the application starts or when it needs to connect to the MongoDB database. Once the connection is established, it can be reused throughout the application's lifecycle, and you don't need to establish a new connection every time you interact with the database.

The line export default connectDB; exports the connectDB function, allowing other parts of the application to use it to establish a connection to the MongoDB database.

Next Auth

NextAuth.js. NextAuth.js is a popular authentication library for Next.js applications. It simplifies the implementation of authentication by providing a set of pre-built authentication providers, such as OAuth, JWT, email/password, and more.

In this application, we have a custom signIn/signOut page, and in that note, we have already set up the database connection and made sure we can sign up. In this phase, we need to have what we call the protection of pages, which is crucial to maybe protect important data. By this, we are using next-auth as the provider, where we start by setting up the authentication. For this we create an API provided by next-auth, through the path api/auth/[...nextuth]

import NextAuth from "next-auth"

import { options } from "./options"

const handler = NextAuth(options)

export { handler as GET, handler as POST }

This code snippet is responsible for initializing the NextAuth authentication library and exporting the handler for the /api/auth route. This allows the application to handle authentication requests for both GET and POST methods.

The spread operator (...) is used in the api/auth/[...nextuth] route to create a catch-all route. This means that any request that starts with /api/auth will be handled by the NextAuth handler, regardless of the rest of the path. This is useful for handling authentication requests that come from different parts of the application.

For example, if the application has a sign-in page at /auth/signin, then the api/auth/[...nextuth] route will handle the request to that page. This means that the application does not need to create a separate route for the sign-in page.

Options

We have an import of the options, which provides an array of different providers. We set up an object, and in this case, we have added it in a separate file where we have an array of providers like, Github, Google and Credetials. Credentials are where we have a custom database you are fetching the user from.

import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials"

export const options: NextAuthOptions = {

    providers: [
        CredentialsProvider({
          // The name to display on the sign in form (e.g. 'Sign in with...')
          name: 'Credentials',

          credentials: {
            username: {  },
            password: {  }
          },
          credentials: {
            username: { label: "Username", type: "text", placeholder: "jsmith" },
            password: { label: "Password", type: "password" }
          },

          async authorize(credentials, req) {

            const res = await fetch("/your/endpoint", {
              method: 'POST',
              body: JSON.stringify(credentials),
              headers: { "Content-Type": "application/json" }
            })
            const user = await res.json()

            // If no error and we have user data, return it
            if (res.ok && user) {
              return user
            }
            // Return null if user data could not be retrieved
            return null
          }
        })
      ]
}

The credentials are used to generate a suitable form on the sign-in page. You can specify whatever fields you are expecting to be submitted. Domain, username, password, 2FA token, etc. You can pass any HTML attribute to the <input> tag through the object. On the authorize function, You need to provide your own logic here that takes the credentials. You should note that this below is usually some random string.

You might note that we have an object, credentials with data objects username and a password. When we are creating a custom login, we will have to leave these objects empty. Leaving them with the input variables means that it is going to generate a login form for us.

NEXTAUTH_SECRET = '4sQLuAOp8ST6ettlsBX0CI2toLKb7KT8l81+JV+JIc0='

After having setup this, next is usually to set up a session provider, which is a client component. For a client to access the data that we get from the session, we need to create a AuthProvider that acts the same way as the react context. We do that by calling the SessionProivder. It is a Provider to wrap the app in to make session data available globally. Can also be used to throttle the number of requests to the endpoint /api/auth/session. We wrap the mainlayout with the session provider, now AuthSessionProvider created. The reason we created a separate file is that session provider is a client component.

'use client'

import React from 'react'

import { SessionProvider} from 'next-auth/react'
import { TLayoutProp } from '@/types/types'

const AuthSessionProvider: React.FC<TLayoutProp> = ({children}) => {
  return (
    <SessionProvider>
      {children}
    </SessionProvider>
  )
}

export default AuthSessionProvider

Next has a middleware file which is supposed to be in the same layer as the app directory.

Middleware in NextJS auth will require one line which will be used to protect the entire site NextJS Middleware. When you add this piece of code, it protects the entire website. We can also protect certain pages by adding the matcher line of code.

On this trend, remember we have to have the data sent to the database, so we can verify that the user has already been registered. In next Auth, this is done on the option (can call it any name) object. It is good to note that option is called in the route of the next-Auth; and remember this is an API for Next Authentication.

CredentialsProvider({
  // The name to display on the sign in form (e.g. 'Sign in with...')
  name: 'Credentials',

  credentials: {
    userEmail: { },
    userPassword: {  },
  },

  async authorize(credentials, req) {

    const {userEmail, userPassword} = credentials ?? {}
    try {
      connectDB();
      const user = await User.findOne({user_email: userEmail})

      if (!user) {
        return
      }

      const matchPassword = await bcrypt.compare(userPassword || '', user.user_password);

      if (matchPassword) {
        return user ;
      }

      return

      } catch (error) {
        console.log(error);
      }
    }
  })

On this code, we have the credentials. As noted earlier, credentials can be an empty project if we have our own custom login screens. Now in the authorize function the credentials receives data from the user, where the data is used to verify the user. On this code, we have taken the credentials and destructured them into an object where we can have the email and the password for look up in the database. The user is first looked for using the email, and if the user is available we then fetch the password for comparison, where if the user is available, the response will be the user. Of not, the response will have an error, meaning the password do not match.

We then have the getServerSession. In NextAuth.js, getServerSession is a server-side helper function used to retrieve the current user's session information. It's particularly useful for securing server-side rendered pages and API routes by making authorization decisions based on the user's session data.

Session in next-auth

When trying to get the session for this project, I noticed that I could not get the user data I returned from the database (I have only verified this when using a custom database, I have not tested for the providers). In options, yes I return the user, but when the session becomes available, and I use getServerSession or the useSession hook, no data is returned. When using the Credentials' provider in NextAuth.js, the data from the user is not returned by default. This behavior is intentional due to the inherent security risks associated with handling credentials such as usernames and passwords. The Credentials' provider allows you to handle signing in with arbitrary credentials, but it comes with the constraint that users authenticated in this manner are not persisted in the database. Consequently, the Credentials' provider can only be used if JSON Web Tokens (JWTs) are enabled for sessions

The authorize function should return either a user object, which indicates the credentials are valid, or null if the credentials are not valid. If you return an object, it will be persisted to the JWT and the user will be signed in. If you return null, an error will be displayed advising the user to check their details.

When you want to display the user’s name and picture when they log in using NextAuth.js, you can achieve this by customizing the session callback to include the necessary information. Here’s a general approach on how to do it:

  1. Modify the authorize function in your [...nextauth].js configuration file to return the user object with the desired fields, such as name and image1.
  2. Use the callbacks to customize the JWT and session tokens. Specifically, you can use the jwt callback to add any additional user data to the JWT, and the session callback to add this data to the session object1.

Note that this is an example of how you might configure your callbacks, which are added to the options directory

callbacks: {
  async jwt({ token, user }) {
    // If the user object is available, add the user's name and picture to the token
    if (user) {
      token.name = user.name;
      token.picture = user.image;
    }
    return token;
  },
  async session({ session, token }) {
    // Add the user's name and picture to the session object
    session.user.name = token.name;
    session.user.image = token.picture;
    return session;
  }
}

Exports in JavaScript/TypeScript

In JavaScript and TypeScript, there are two primary ways to export functionality from a module: named exports and default exports.

Named Exports:

With named exports, you export specific functions or variables by name. In this case, you would import the connectDB function using its name when importing in another file

export const connectDB = async () => {
  // ...
};
// Module with named exports
export const functionA = () => { /* ... */ };
export const functionB = () => { /* ... */ };

In Another File, you can have

import { connectDB } from './yourModule';
// Importing named exports
import { functionA, functionB } from './yourModule';

Named exports are useful when you want to export multiple functions, variables, or classes from a module, and you want to be explicit about which ones you are importing in another file. With named exports, you can have multiple functions, variables, or classes within the same file, and each of them can be exported independently. This is useful when you want to organize related functionality within a single module.

Default Export:

With a default export, you are exporting a single "default" entity from your module. When importing, you can give it any name you want:

const connectDB = async () => {
  // ...
};

export default connectDB;

So I can have the name Change to whatever I want like here:

import myCustomName from './yourModule';

Default exports are convenient when you have a single main entity to export from a module. It simplifies the import syntax because you can choose any name for the imported entity.

MDX Files

joe_jngigi_beta's People

Contributors

joe-jngigi avatar

Watchers

 avatar

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.