Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# API
AXIOM_DOMAIN=https://api.axiom.co
AXIOM_TOKEN=
BETTER_AUTH_SECRET=
DATABASE_URL=postgres://postgres:supersecret@localhost:5432/universal-starter?sslmode=require
DATASET_NAME=universal-
EMAIL_FROM=noreply@yourapp.com
EMAIL_FROM_NAME=Your App Name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add quotes around the value with spaces.

The value "Your App Name" contains spaces and should be quoted to ensure proper parsing.

Apply this diff:

-EMAIL_FROM_NAME=Your App Name
+EMAIL_FROM_NAME="Your App Name"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
EMAIL_FROM_NAME=Your App Name
EMAIL_FROM_NAME="Your App Name"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 8-8: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In .env.example around line 8, the EMAIL_FROM_NAME value contains spaces and
must be quoted to ensure correct parsing by dotenv-style loaders; update the
line to wrap the value in double quotes (e.g. EMAIL_FROM_NAME="Your App Name")
so the value is read as a single string.

EMAIL_PROVIDER=resend
NODEMAILER_PASS=your_app_password
NODEMAILER_SERVICE=gmail
NODEMAILER_USER=your_email@gmail.com
PLUNK_API_KEY=your_plunk_api_key_here
REDIS_URL=redis://default:supersecret@localhost:6379
RESEND_API_KEY=
SMTP_HOST=smtp.gmail.com
SMTP_PASS=your_app_password
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your_email@gmail.com
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ yarn-error.*

# local env files
.env*.local
.env

# Expo
.expo/
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ docker compose -f docker/docker-compose.dev.yml up --build
docker compose -f docker/docker-compose.prod.yml up --build -d
```

# Inspiration

- https://www.better-t-stack.dev/

## License

MIT
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"@better-auth/expo": "^1.3.32",
"@better-auth/expo": "^1.4.1",
"@bogeychan/elysia-etag": "^0.1.0",
"@bogeychan/elysia-logger": "^0.1.10",
"@elysiajs/cors": "^1.4.0",
Expand All @@ -27,7 +27,7 @@
"@universal/queue-kit": "workspace:*",
"@universal/redis": "workspace:*",
"@universal/shared": "workspace:*",
"better-auth": "^1.3.32",
"better-auth": "^1.4.1",
"elysia": "^1.4.16",
"elysia-rate-limit": "^4.4.2",
"ioredis": "^5.8.2",
Expand Down
96 changes: 48 additions & 48 deletions apps/api/src/plugins/auth-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,56 @@ import { formatErrorResponse } from "../utils/format-response";
import { withAuth } from "./auth";

export const requireAuth = () =>
new Elysia({ name: "requireAuth" })
.use(withAuth())
.onBeforeHandle({ as: "scoped" }, ({ session, user, set }) => {
if (!session || !user) {
const { code: status, message } = getStatusCode(401);
set.status = status as number;
const envelope = formatErrorResponse({
code: message,
problem: {
status: status as number,
title: "User is not authenticated",
type: "urn:problem-type:authentication-error",
},
});
new Elysia({ name: "requireAuth" })
.use(withAuth())
.onBeforeHandle({ as: "scoped" }, ({ session, user, set }) => {
if (!(session && user)) {
const { code: status, message } = getStatusCode(401);
set.status = "Unauthorized";
const envelope = formatErrorResponse({
code: message,
problem: {
status: status as number,
title: "User is not authenticated",
type: "urn:problem-type:authentication-error",
},
});

throw new Response(JSON.stringify(envelope), {
status: status as number,
headers: {
"Content-Type": "application/problem+json",
"www-authenticate": 'Bearer realm="api"',
},
});
}
});
throw new Response(JSON.stringify(envelope), {
status: status as number,
headers: {
"Content-Type": "application/problem+json",
"www-authenticate": 'Bearer realm="api"',
},
});
}
});

export const requireRole = (roles: string[]) =>
new Elysia({ name: "requireRole" })
.use(withAuth())
.use(requireAuth())
.onBeforeHandle({ as: "scoped" }, ({ user, set }) => {
const { role } = user || {};
const hasRole = role && roles.includes(role);
new Elysia({ name: "requireRole" })
.use(withAuth())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Calling .use(withAuth()) before .use(requireAuth()) duplicates withAuth decoration because requireAuth() already applies withAuth, so Elysia will throw a "property already decorated" error when auth is registered twice; drop the extra .use(withAuth()) to avoid the crash. [logic error]

Severity Level: Minor ⚠️

Suggested change
.use(withAuth())
Why it matters? ⭐

requireAuth() already calls .use(withAuth()), so the extra .use(withAuth()) before it decorates auth twice on the same Elysia instance and will surface the "property already decorated" failure. Dropping the redundant decorator fixes a real runtime error tied directly to this PR diff.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** apps/api/src/plugins/auth-guards.ts
**Line:** 34:34
**Comment:**
	*Logic Error: Calling `.use(withAuth())` before `.use(requireAuth())` duplicates `withAuth` decoration because `requireAuth()` already applies `withAuth`, so Elysia will throw a "property already decorated" error when `auth` is registered twice; drop the extra `.use(withAuth())` to avoid the crash.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

.use(requireAuth())
.onBeforeHandle({ as: "scoped" }, ({ user, set }) => {
const { role } = user || {};
const hasRole = role && roles.includes(role);

if (!hasRole) {
const { code: status, message } = getStatusCode(403);
if (!hasRole) {
const { code: status, message } = getStatusCode(403);

set.status = status as number;
const envelope = formatErrorResponse({
code: message,
problem: {
status: status as number,
title: "User is not authorized",
type: "urn:problem-type:authorization-error",
},
});
throw new Response(JSON.stringify(envelope), {
status: status as number,
headers: {
"Content-Type": "application/problem+json",
},
});
}
});
set.status = "Forbidden";
const envelope = formatErrorResponse({
code: message,
problem: {
status: status as number,
title: "User is not authorized",
type: "urn:problem-type:authorization-error",
},
});
throw new Response(JSON.stringify(envelope), {
status: status as number,
headers: {
"Content-Type": "application/problem+json",
},
});
}
});
10 changes: 5 additions & 5 deletions apps/api/src/plugins/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { auth } from '@src/lib/auth';
import Elysia from 'elysia';
import { auth } from "@src/lib/auth";
import Elysia from "elysia";

export const withAuth = () =>
new Elysia({ name: 'withAuth' })
.decorate('auth', auth)
new Elysia({ name: "withAuth" })
.decorate("auth", auth)
.derive(
{ as: 'scoped' },
{ as: "scoped" },
async function getSession({ request: { headers } }) {
const result = await auth.api.getSession({ headers });
Comment on lines 9 to 10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: auth.api.getSession expects to receive the original request object so it can read headers/cookies; by only passing { headers } the underlying call now sees request as undefined and will throw when it tries to access request.headers, preventing authentication from working. [type error]

Severity Level: Minor ⚠️

Suggested change
async function getSession({ request: { headers } }) {
const result = await auth.api.getSession({ headers });
async function getSession({ request }) {
const result = await auth.api.getSession({ request });
Why it matters? ⭐

The helper on auth.api actually walks through request.headers/cookies, so feeding it an object that only has headers leaves request undefined and will throw when it tries to dereference request.headers. Passing the entire request fixes a real runtime authentication bug rather than touching style.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** apps/api/src/plugins/auth.ts
**Line:** 9:10
**Comment:**
	*Type Error: `auth.api.getSession` expects to receive the original `request` object so it can read headers/cookies; by only passing `{ headers }` the underlying call now sees `request` as undefined and will throw when it tries to access `request.headers`, preventing authentication from working.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

if (!result) {
Expand Down
Loading
Loading