-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from mpiorowski/feat/upgrade
Big Upgrade
- Loading branch information
Showing
89 changed files
with
2,600 additions
and
2,416 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,33 +4,38 @@ https://sgsg.bearbyte.org/ | |
|
||
## What is SGSG? | ||
|
||
It is an open-source full-stack application with two main principles in mind: **PERFORMANCE** and **SIMPLICITY**. | ||
It is an open-source full-stack application with two main principles in mind: **PERFORMANCE** and **SIMPLICITY**. | ||
The idea is that you can take this template and use it to build almost anything you need, and it will scale very well. | ||
|
||
Also, this is not the next **dev** template. It has everything you need to push it to production: Nginx configuration, Docker deployments, GitHub Actions, Grafana logging, etc. | ||
|
||
## Alternative | ||
|
||
If you need something a little more complicated (Rust, microservices, PostgreQSL, cloud deployment), feel free to check out the second project I am running: | ||
**[Rusve](https://github.com/mpiorowski/rusve)** | ||
|
||
## Architecture | ||
|
||
As the name suggests, the app contains four main components: | ||
|
||
- **[SvelteKit](https://kit.svelte.dev/)** - Svelte currently is what I believe the best frontend framework. If you've never worked with it, don't worry; it's super easy to pick up. | ||
As an example, developers from my team who were familiar with NextJS were able to grasp it in just one week and start coding web apps. Trust me, once you try it, it's hard to go back to anything else. | ||
As an example, developers from my team who were familiar with NextJS were able to grasp it in just one week and start coding web apps. Trust me, once you try it, it's hard to go back to anything else. | ||
- **[Go](https://go.dev/)** - The easiest backend on the market. Don't confuse simplicity with inefficiency; it's almost impossible to build a bad server using it. | ||
- **[SQLite](https://www.sqlite.org/index.html)** - The most used database in the world. You might be skeptical about using SQLite for production, but trust me, unless you're building the next Netflix, this is all you need. | ||
It will be faster than your PostgreSQL or MySQL because the database sits next to the backend, eliminating one network connection. Also changing it to **PostgreSQL/MySQL** should take only a minute :). | ||
It will be faster than your PostgreSQL or MySQL because the database sits next to the backend, eliminating one network connection. Also changing it to **PostgreSQL/MySQL** should take only a minute :). | ||
- **[gRPC](https://grpc.io/)** - Now we are delving into something a little bit harder to grasp, but I believe it's totally worth it. Two of the most important things it gives us: | ||
- **[Typesafety](https://protobuf.dev/)** - Thanks to protobuf, there is amazing type safety across the whole project, regardless of the language (not only for TypeScript, hi tRPC). Trust me; this is phenomenal. | ||
If you add one "field" to your User object, both JavaScript and Go will lint, pointing out exactly where you need to take care of it. Adding a new language like Java or Rust? Type safety for them as well. | ||
- **[Streaming](https://grpc.io/docs/what-is-grpc/core-concepts/#server-streaming-rpc)** - gRPC allows streaming data, which, for larger datasets, offers incredible performance. Add to this Go routines, and you can create the most amazing backend services. | ||
Load measurements concurrently start doing calculations on them and send them via streams as soon as any of them finish. No waiting, no blocking. | ||
- **[Typesafety](https://protobuf.dev/)** - Thanks to protobuf, there is amazing type safety across the whole project, regardless of the language (not only for TypeScript, hi tRPC). Trust me; this is phenomenal. | ||
If you add one "field" to your User object, both JavaScript and Go will lint, pointing out exactly where you need to take care of it. Adding a new language like Java or Rust? Type safety for them as well. | ||
- **[Streaming](https://grpc.io/docs/what-is-grpc/core-concepts/#server-streaming-rpc)** - gRPC allows streaming data, which, for larger datasets, offers incredible performance. Add to this Go routines, and you can create the most amazing backend services. | ||
Load measurements concurrently start doing calculations on them and send them via streams as soon as any of them finish. No waiting, no blocking. | ||
|
||
If You have any questions, feel free to ask them in Discussions or Issues. I hope this will be helpful :). | ||
|
||
## Additional features | ||
|
||
- **Stripe Subscription** - Fully working subscription flow. | ||
- **No TypeScript Build, Fully Typed with JSDocs** - Despite the absence of a TypeScript build, the code remains fully typed using JSDocs. While this approach may be somewhat controversial due to requiring more lines of code, the more I work with pure JSDocs, the more I appreciate its versatility. | ||
It supports features like Enums, as const, and even Zod's z.infer<typeof User>, eliminating the need for the entire TypeScript build step. | ||
It supports features like Enums, as const, and even Zod's z.infer<typeof User>, eliminating the need for the entire TypeScript build step. | ||
- **Fully Tested** - Backend tested using go native modules, frontend using vitest and playwright. | ||
- **Very Secure OAuth Implementation** - Utilizes the Phantom Token Approach with additional client-to-server authorization using an RSA key, ensuring robust security. | ||
- **Minimal External Libraries** - Emphasizes a minimalistic approach to external libraries. From my experience, relying less on external dependencies contributes to code maintainability. This approach makes it easier to make changes even after years. It's simply the way to go. | ||
|
@@ -46,12 +51,14 @@ Thx ChatGPT for these bullet points :). | |
## Test | ||
|
||
Backend: | ||
|
||
``` | ||
cd server | ||
ENV=test go test ./... | ||
go test -v ./... | ||
``` | ||
|
||
Frontend: | ||
|
||
``` | ||
cd client | ||
npm run test:unit | ||
|
@@ -61,18 +68,30 @@ npm run test:integration | |
## Proto | ||
|
||
Whenever You change proto definitions, always remember to generate new types: | ||
|
||
``` | ||
sh proto.sh | ||
``` | ||
|
||
## Backup | ||
|
||
To backup the database, run the following command: | ||
|
||
``` | ||
sqlite3 server/system/db.sqlite3 ".backup 'server/system/db.sqlite3.bak'" | ||
``` | ||
|
||
## Deployment | ||
|
||
The only prerequisites are `Docker` and `Docker Compose`. | ||
The only prerequisites are `Docker` and `Docker Compose`. | ||
|
||
Afterward, the only task remaining is to configure environment variables according to the deployment. No need for .env files or tedious copy/pasting — just straightforward environment variables, either configured on the system or written inline. | ||
|
||
### Development | ||
|
||
``` | ||
STRIPE_API_KEY=STRIPE_API_KEY \ | ||
STRIPE_PRICE_ID=STRIPE_PRICE_ID \ | ||
GOOGLE_CLIENT_ID=GOOGLE_CLIENT_ID \ | ||
GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET \ | ||
GITHUB_CLIENT_ID=GITHUB_CLIENT_ID \ | ||
|
@@ -86,13 +105,15 @@ docker compose up --build | |
Let's embark on the full journey: | ||
|
||
1. Generate new RSA keys and push them to your repository: | ||
|
||
``` | ||
openssl genpkey -algorithm RSA -out private.key -pkeyopt rsa_keygen_bits:2048 | ||
openssl rsa -pubout -in private.key -out public.key | ||
mv private.key ./client/src/lib/server/private.key | ||
mv public.key ./server/public.key | ||
``` | ||
|
||
2. Purchase two servers (Hetzner CPX11 costs 5 euros/month each), one for the client and one for the server, with SSH key authorization. | ||
3. Add three repository secrets to GitHub: | ||
- SSH_KEY: private version of the SSH key used for logging into servers | ||
|
@@ -103,28 +124,34 @@ mv public.key ./server/public.key | |
From here, you need to follow these instructions on both servers: | ||
|
||
5. Log in and create a new SSH key: | ||
|
||
``` | ||
ssh-keygen -t ed25519 -C [email protected] | ||
``` | ||
|
||
6. Add the `.pub` version as the deployment key in your repository settings. | ||
7. Download the repository into server. | ||
8. Finish setting up environment variables: | ||
- Change domains in `docker-compose.client.yml` / `docker-compose.server.yml` | ||
- Add environment variables to your system, e.g., using Fish: `set -Ux GOOGLE_CLIENT_ID 123456789`. | ||
- You can find all needed environment variables in `docker-compose.client.yml` / `docker-compose.server.yml`. Don't miss out on the `LOKI_URL` one — you can find it on the Grafana Cloud Portal -> Loki Details. | ||
- Change domains in `docker-compose.client.yml` / `docker-compose.server.yml` | ||
- Add environment variables to your system, e.g., using Fish: `set -Ux GOOGLE_CLIENT_ID 123456789`. | ||
- You can find all needed environment variables in `docker-compose.client.yml` / `docker-compose.server.yml`. Don't miss out on the `LOKI_URL` one — you can find it on the Grafana Cloud Portal -> Loki Details. | ||
9. Generate certificates for your domain: | ||
|
||
``` | ||
sudo apt-get install certbot python3-certbot-nginx | ||
sudo certbot certonly --nginx -d example.com | ||
sudo apt-get install certbot | ||
sudo certbot certonly --standalone -d example.com | ||
sudo certbot renew --dry-run | ||
sudo systemctl disable nginx | ||
``` | ||
10. Install the Grafana Loki plugin for Docker: | ||
|
||
10. Install the Grafana Loki plugin for Docker (check the latest version on the Grafana website): | ||
|
||
``` | ||
docker plugin install grafana/loki-docker-driver:2.8.2 --alias loki --grant-all-permissions | ||
``` | ||
|
||
11. Restart server. | ||
12. Log in and run the app for the first time to ensure everything is correct: | ||
|
||
``` | ||
cd you-repo | ||
git checkout release | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,82 @@ | ||
import { COOKIE_DOMAIN } from "$env/static/private"; | ||
import { grpcSafe } from "$lib/safe"; | ||
import { server } from "$lib/server/grpc"; | ||
import { UserRole } from "$lib/proto/proto/UserRole"; | ||
import { grpcSafe, server } from "$lib/server/grpc"; | ||
import { logger, perf } from "$lib/server/logger"; | ||
import { createMetadata } from "$lib/server/metadata"; | ||
import { redirect } from "@sveltejs/kit"; | ||
import { building } from "$app/environment"; | ||
|
||
/** @type {import('@sveltejs/kit').Handle} */ | ||
export async function handle({ event, resolve }) { | ||
const end = perf("Auth"); | ||
if (building) { | ||
return await resolve(event); | ||
} | ||
|
||
const end = perf("auth"); | ||
event.locals.user = { | ||
id: "", | ||
created: "", | ||
updated: "", | ||
deleted: "", | ||
email: "", | ||
avatar: "", | ||
role: 0, | ||
role: UserRole.ROLE_UNSET, | ||
sub: "", | ||
subscriptionId: "", | ||
subscriptionEnd: "", | ||
_deleted: "deleted", | ||
_subscriptionEnd: "subscriptionEnd", | ||
subscription_id: "", | ||
subscription_end: "", | ||
subscription_check: "", | ||
subscription_active: false, | ||
}; | ||
|
||
if (event.url.pathname === "/auth") { | ||
event.cookies.set("token", "", { | ||
domain: COOKIE_DOMAIN, | ||
path: "/", | ||
maxAge: 0, | ||
}); | ||
return await resolve(event); | ||
} | ||
|
||
const token = event.cookies.get("token"); | ||
/** | ||
* Check if the user is coming from the oauth flow | ||
* If so, set a temporary cookie with the token | ||
* On the next request, the new token will be used | ||
*/ | ||
let token = event.url.pathname.includes("/token/") | ||
? event.url.pathname.split("/token/")[1] | ||
: ""; | ||
if (token) { | ||
event.cookies.set("token", token, { | ||
path: "/", | ||
maxAge: 10, | ||
}); | ||
throw redirect(302, "/"); | ||
} | ||
|
||
token = event.cookies.get("token") ?? ""; | ||
if (!token) { | ||
logger.info("No token"); | ||
throw redirect(302, "/auth"); | ||
throw redirect(302, "/auth?error=1"); | ||
} | ||
|
||
const metadata = createMetadata(token); | ||
/** @type {import("$lib/safe").Safe<import("$lib/proto/proto/AuthResponse").AuthResponse__Output>} */ | ||
/** @type {import("$lib/server/safe").Safe<import("$lib/proto/proto/AuthResponse").AuthResponse__Output>} */ | ||
const auth = await new Promise((res) => { | ||
server.Auth({}, metadata, grpcSafe(res)); | ||
}); | ||
if (auth.error || !auth.data.tokenId || !auth.data.user) { | ||
if (!auth.success || !auth.data.token || !auth.data.user) { | ||
logger.error("Error during auth"); | ||
throw redirect(302, "/auth"); | ||
throw redirect(302, "/auth?error=1"); | ||
} | ||
|
||
event.locals.user = auth.data.user; | ||
event.locals.token = auth.data.tokenId; | ||
event.locals.token = auth.data.token; | ||
// logger.debug(event.locals.user, "user"); | ||
|
||
end(); | ||
const response = await resolve(event); | ||
// max age is 30 days | ||
// max age is 7 days | ||
response.headers.append( | ||
"set-cookie", | ||
`token=${auth.data.tokenId}; HttpOnly; SameSite=Lax; Secure; Max-Age=2592000; Path=/; Domain=${COOKIE_DOMAIN}`, | ||
`token=${auth.data.token}; HttpOnly; SameSite=Lax; Secure; Max-Age=604800; Path=/`, | ||
); | ||
return response; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.