ColinLefter / Accord

A real-time privacy-first social media platform leveraging feature-rich direct messaging text channels. Built as part of the course project for COSC 310 at UBC.
5 stars 1 forks source link

Writing REST API Endpoints #70

Open ColinLefter opened 8 months ago

ColinLefter commented 8 months ago

As we begin to integrate database functionality across our components, we will need to process all user input with our central database. As all user-input is received on the client-side, you will need to process that data in the server (Node.js). To do this, you cannot directly interact with the database in the onSubmit method for your forms, but will need to send an API request to the API endpoint that is responsible for handling that event.

You will need to define REST API endpoints under the pages/api directory where for every form you will write a corresponding handler that intakes that data and sends it to MongoDB and then returns some data back to that component in the frontend. In summary, the process is as follows:

  1. In your frontend component, write an HTTP request to the corresponding API endpoint in the server that is designed to handle that event. This is done in every onSubmit request or wherever necessary.
  2. Write the API endpoint that will handle that request. Depending on the process executed, you will need to return some data back to the original frontend component.
  3. Receive all status updates from the API endpoint in the frontend component and handle them accordingly.

Example -- Login Page

Frontend Login Form Component

/**
 * The Login component provides an entry point to the application, displaying a form
 * that accepts a username and password. Upon form submission, it sends these credentials
 * to the /api/login endpoint for verification and handles the response to either
 * redirect the user to the main application page or display error messages.
 */
export function Login() {
  const router = useRouter(); // To handle page redirection after the user logs in

  // State hooks for form data and validation errors
  const [formData, setFormData] = useState<{userName: string, password: string}>({
    userName: "",
    password: "",
  });

  // To facilitate client-side validation
  const [usernameError, setUsernameError] = useState(''); // The message for an incorrect username
  const [passwordError, setPasswordError] = useState(''); // The message for a correct username but incorrect password

  /**
   * Handles form submission, sending the login request to the server and processing
   * the response to either proceed to the application or show error messages.
   *
   * @param event - The form submission event
   */
  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    const response = await fetch('/api/login', { // Establishing a promise
      method: 'POST', // As we are dealing with authentication, this is the most appropriate method
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(formData), // The data that is being sent
    });

    const data = await response.json(); // Awaiting the resolution of the promise

    if (response.ok) { // If the response was successful
      router.push('/accord'); // Redirect the user to the main application page
    } else {
      if (data.error === 'Invalid username') {
        setUsernameError('Invalid username. Please try again.'); // Client-side validation
      } else if (data.error === 'Invalid password') {
        setPasswordError('Invalid password. Please try again.'); // Client-side validation
      }
    }
  };
// more code follows
}

Backend API endpoint

/**
 * Handles the POST request for user login, validating the user credentials against the database.
 * Responds with a success message and status code 200 if the credentials are valid, or an error
 * message and status code 401 for invalid credentials, and 500 for any internal server errors.
 *
 * @param req - The incoming Next.js API request object, containing the username and password.
 * @param res - The outgoing Next.js API response object used to send back the result.
 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    const { userName, password } = req.body; // Intaking the data that has been sent from the client-side
    let client: MongoClient | null = null; // We need to assign something to the client so TypeScript is aware that it can be null if the connection fails

    try {
      client = new MongoClient(getMongoDbUri());
      await client.connect();
      const db = client.db('Accord');

      const accountsCollection = db.collection("Accounts");
      // Querying the database by the username we received
      const user = await accountsCollection.findOne({ userName: userName }); // IMPORTANT: The findOne method returns a promise, so we need to await the resolution of the promise first

      if (user && user.password === password) {
        return res.status(200).json({ message: 'Login successful' });
      } else {
        if (!user) {
          return res.status(401).json({ error: 'Invalid username' });
        } else {
          return res.status(401).json({ error: 'Invalid password' });
        }
      }
    } catch (error) {
      console.error(error);
      return res.status(500).json({ error: 'Internal server error' });
    } finally {
      if (client) {
        await client.close();
      }
    }
  } else {
    // Handle any requests other than POST
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}