Skip to content

Commit bcc2eb0

Browse files
Copilotelifouts
andauthored
Fix review comments: AASA content-type, proxy header spoofing, API_BASE_URL const mutation, installer guard, docs (#143)
* Initial plan * Address PR review comments: AASA content-type, proxy header security, template check, API_BASE_URL fix, docs update Co-authored-by: elifouts <116454864+elifouts@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>
1 parent 4a2caa7 commit bcc2eb0

File tree

7 files changed

+59
-31
lines changed

7 files changed

+59
-31
lines changed

backend/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ DEVBITS_JWT_SECRET=replace-with-64+char-random-secret
1111
DEVBITS_ADMIN_KEY=replace-with-base64-or-random-admin-key
1212
DEVBITS_ADMIN_LOCAL_ONLY=0
1313
DEVBITS_API_ADDR=0.0.0.0:8080
14+
# Set to "true" when running behind a trusted reverse proxy (e.g. AWS ALB/nginx)
15+
# that sets the X-Forwarded-Proto header. Do NOT enable if the backend is directly
16+
# internet-facing, as the header would be client-controlled.
17+
DEVBITS_TRUST_PROXY=false

backend/api/internal/handlers/media_routes.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import (
2020

2121
const uploadDir = "uploads"
2222

23+
// trustProxy is evaluated once at startup to avoid repeated os.Getenv calls
24+
// on every upload request. Set DEVBITS_TRUST_PROXY=true only when the backend
25+
// runs behind a trusted reverse proxy (e.g. AWS ALB) that sets X-Forwarded-Proto.
26+
var trustProxy = os.Getenv("DEVBITS_TRUST_PROXY") == "true"
27+
2328
var allowedImageExtensions = map[string]struct{}{
2429
".jpg": {},
2530
".jpeg": {},
@@ -207,13 +212,15 @@ func UploadMedia(context *gin.Context) {
207212
}
208213

209214
scheme := "http"
210-
if forwardedProto := strings.TrimSpace(context.GetHeader("X-Forwarded-Proto")); forwardedProto != "" {
211-
scheme = strings.ToLower(strings.TrimSpace(strings.Split(forwardedProto, ",")[0]))
212-
if scheme != "http" && scheme != "https" {
213-
scheme = "http"
214-
}
215-
} else if context.Request.TLS != nil {
215+
if context.Request.TLS != nil {
216216
scheme = "https"
217+
} else if trustProxy {
218+
if forwardedProto := strings.TrimSpace(context.GetHeader("X-Forwarded-Proto")); forwardedProto != "" {
219+
scheme = strings.ToLower(strings.TrimSpace(strings.Split(forwardedProto, ",")[0]))
220+
if scheme != "http" && scheme != "https" {
221+
scheme = "http"
222+
}
223+
}
217224
}
218225
relativeURL := fmt.Sprintf("/%s/%s", uploadDir, filename)
219226
absoluteURL := fmt.Sprintf("%s://%s%s", scheme, context.Request.Host, relativeURL)

backend/api/main.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,18 @@ func main() {
195195
} else {
196196
log.Printf("INFO: admin UI available at /admin (key-protected)")
197197
}
198-
router.StaticFile("/apple-app-site-association", "./api/static/apple-app-site-association")
199-
router.StaticFile("/.well-known/assetlinks.json", "./api/static/assetlinks.json")
198+
router.GET("/apple-app-site-association", func(c *gin.Context) {
199+
c.Header("Content-Type", "application/json")
200+
c.File("./api/static/apple-app-site-association")
201+
})
202+
router.GET("/.well-known/apple-app-site-association", func(c *gin.Context) {
203+
c.Header("Content-Type", "application/json")
204+
c.File("./api/static/apple-app-site-association")
205+
})
206+
router.GET("/.well-known/assetlinks.json", func(c *gin.Context) {
207+
c.Header("Content-Type", "application/json")
208+
c.File("./api/static/assetlinks.json")
209+
})
200210
router.StaticFile("/privacy-policy", "./api/static/privacy-policy.html")
201211
router.StaticFile("/account-deletion", "./api/static/account-deletion.html")
202212

backend/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/gin-gonic/gin v1.10.0
88
github.com/golang-jwt/jwt/v5 v5.3.1
99
github.com/gorilla/websocket v1.5.3
10+
github.com/joho/godotenv v1.5.1
1011
github.com/lib/pq v1.11.2
1112
github.com/sirupsen/logrus v1.9.3
1213
github.com/stretchr/testify v1.9.0
@@ -28,7 +29,6 @@ require (
2829
github.com/go-playground/validator/v10 v10.20.0 // indirect
2930
github.com/goccy/go-json v0.10.2 // indirect
3031
github.com/google/uuid v1.6.0 // indirect
31-
github.com/joho/godotenv v1.5.1 // indirect
3232
github.com/json-iterator/go v1.1.12 // indirect
3333
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
3434
github.com/kr/text v0.2.0 // indirect

backend/scripts/install-aws-systemd-service.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ if [[ ! -x "$BINARY_PATH" ]]; then
3030
exit 1
3131
fi
3232

33+
if [[ ! -f "$TEMPLATE_PATH" ]]; then
34+
echo "Missing systemd service template: $TEMPLATE_PATH" >&2
35+
echo "Ensure the deploy/systemd/devbits-api.service template is present." >&2
36+
exit 1
37+
fi
38+
3339
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
3440
echo "Service user does not exist: $SERVICE_USER" >&2
3541
exit 1

frontend/DEEP_LINK_SETUP.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ Alternative (if you use Play App Signing)
5555

5656
4. Update `assetlinks.json`
5757

58-
- Open `frontend/public/.well-known/assetlinks.json` and replace `<SHA256_CERT_FINGERPRINT>` with the SHA-256 fingerprint (format: uppercase hex with colons or without; either is accepted by Android).
58+
- Open `backend/api/static/assetlinks.json` and replace `<SHA256_CERT_FINGERPRINT>` with the SHA-256 fingerprint (format: uppercase hex with colons or without; either is accepted by Android).
5959

6060
5. Deploy static files to your server/domain
6161

62-
- Host `frontend/public/apple-app-site-association` and `frontend/public/.well-known/assetlinks.json` at:
63-
- https://devbits.app/apple-app-site-association
64-
- https://devbits.app/.well-known/assetlinks.json
62+
- The Go backend now serves these files directly from `backend/api/static/`. No separate static hosting is needed.
63+
- `https://devbits.app/apple-app-site-association` → served from `backend/api/static/apple-app-site-association` (also available at `/.well-known/apple-app-site-association`)
64+
- `https://devbits.app/.well-known/assetlinks.json` → served from `backend/api/static/assetlinks.json`
65+
- To update these files, edit them in `backend/api/static/` and redeploy the backend.
6566

6667
6. Verify
6768

frontend/services/api.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -318,28 +318,28 @@ const buildBaseUrlList = (...candidates: Array<string | null | undefined>) => {
318318
return urls;
319319
};
320320

321-
export const API_BASE_URL = normalizeBaseUrl(getDefaultBaseUrl());
322-
323-
// Defensive runtime validation: if the resolved URL is malformed (e.g. "http://"
324-
// with no host) fall back to the public host and log an error so developers
325-
// can see the problem in the Metro/Expo console.
326-
try {
327-
const checkUrl = new URL(API_BASE_URL);
328-
if (!checkUrl.hostname) {
329-
// eslint-disable-next-line no-console
330-
console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.app", API_BASE_URL);
331-
// normalize and overwrite
332-
(exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.app");
333-
}
334-
} catch (e) {
335-
// If parsing fails entirely, fallback and log.
321+
// Validate the resolved URL at module load time; if it is malformed (e.g.
322+
// "http://" with no host) fall back to the public host and log an error so
323+
// developers can see the problem in the Metro/Expo console.
324+
function getValidatedBaseUrl(): string {
325+
const raw = normalizeBaseUrl(getDefaultBaseUrl());
336326
try {
327+
const checkUrl = new URL(raw);
328+
if (!checkUrl.hostname) {
329+
// eslint-disable-next-line no-console
330+
console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.app", raw);
331+
return normalizeBaseUrl("https://devbits.app");
332+
}
333+
return raw;
334+
} catch (e) {
337335
// eslint-disable-next-line no-console
338-
console.error("Failed to parse API_BASE_URL; falling back to https://devbits.app", API_BASE_URL, String(e));
339-
} catch {}
340-
(exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.app");
336+
console.error("Failed to parse API_BASE_URL; falling back to https://devbits.app", raw, String(e));
337+
return normalizeBaseUrl("https://devbits.app");
338+
}
341339
}
342340

341+
export const API_BASE_URL = getValidatedBaseUrl();
342+
343343
const API_FALLBACK_URL = normalizeBaseUrl(
344344
__DEV__ ? "" : "https://devbits.app",
345345
);

0 commit comments

Comments
 (0)