When adding authorization to my backend, I struggled with modifying types and ensuring that only books created by a user could be modified by them. I realized I needed to add an extra field in my book table to record which user created each book. As a result, I had to update my Zod book schema type. However, this caused a type error for the book post endpoint, because the validation expected a created_by field, but I wanted that to be set by the server. To resolve this, I created a separate type for the book post API that did not include the created_by field, allowing the server to set it. Additionally, I modified my backend “get all books” endpoint to return an additional is_owner boolean, so the frontend could easily identify whether the user was the creator of the book. This allowed the frontend to determine which books the user could modify without performing additional mapping to compare usernames.
When adding authorization to my frontend, I initially had difficulty figuring out where to start and how to pass the logged-in state without checking it in every file. I did not want to rely heavily on the frontend cookie, as I thought it would be better to verify login status directly from the backend. Instead, I discovered useOutletContext, which allowed me to pass an isLoggedIn state to child components without repeatedly checking authentication. Getting the setup correct across multiple components required some trial and error. Ultimately, the main challenge of frontend validation was deciding where to implement authentication and how to propagate that knowledge throughout the app.
When deploying my app to the internet, I struggled with git cloning and an SSH issue. After setting up SSH and trying to git clone, the command kept failing even though I had the SSH key. I later realized this was because I didn’t have permissions to manage files in the /srv folder. I used sudo chown xixi:xixi /srv to give myself permissions and was then able to git clone. The second issue was more frustrating. I often use VSCode Remote SSH to navigate files easily, but while upgrading a package on the server, the connection dropped. I tried to SSH in again, but it failed even in the terminal. I accessed the Hetzner console to check SSH, verified it was active, and restarted it, which fixed the issue temporarily. Later, when I SSH-ed via terminal and tried VSCode again, the connection dropped, and restarting SSH showed messages about “memory under pressure” and “flushing cache.” After some time, SSH worked again, and I completed the remaining deployment through the terminal instead of VSCode. VSCode Remote SSH seems to trigger memory issues, though the cause is unclear. The rest of the deployment went smoothly, and I was able to deploy my Node server using PM2. As an aside, using the Hetzner console was a bit challenging as the UX and UI was not great.
My app is not particularly vulnerable to XSS (Cross-Site Scripting) attacks, as it started with many protections. For example, user input is validated and sanitized using Zod schemas on the backend. On the frontend, React automatically escapes content in JSX, preventing basic injection attacks. I also did not use any setInnerHTML, which would bypass React's automatic escaping. However, my app was still at risk of unauthorized script injections. To prevent this, I used a default-configured Content Security Policy (CSP) via the Helmet package, which blocks unauthorized scripts from executing.
My app is protected from CSRF (Cross-Site Request Forgery). CSRF attacks are mitigated by setting public cookie options with SameSite: 'strict' and secure: true. Private cookie options are set to httpOnly: true. The SameSite attribute ensures cookies are only sent from requests originating from my own website. The secure attribute ensures that cookies are only sent via HTTPS, preventing interception. Lastly, httpOnly prevents client-side scripts from accessing the cookie, further reducing the risk of CSRF.
I added rate limiting to my application code using the express-rate-limit package. I added two rate limiting configurations. The first is for the login endpoint, which is stricter, allowing only 10 requests per 15 minutes per IP to prevent brute force attacks. The second is a general API limiter for all other endpoints, allowing 150 requests per 15 minutes. This ensures normal API usage continues while preventing abuse.
I used the Helmet package to set HTTP headers, using Helmet's default CSP directives. The headers I set include content sources, forms, navigation, and security. Content source headers are default-src, script-src, style-src, font-src, img-src, object-src, and script-src-attr, which restrict where content can be loaded from to prevent malicious injections such as XSS. Form and navigation headers are form-action, base-uri, and frame-ancestors, limiting where forms, URLs, and iframes can come from to prevent attacks like form data exfiltration or clickjacking. Lastly, the upgrade-insecure-requests header upgrades HTTP requests to HTTPS, securing the connection and encrypting data to reduce the risk of interception.
I did not do anymore additional work to secure the app.