Skip to content

Commit 7a69c4c

Browse files
committed
fix(quality): more stuff
1 parent d8cf8a9 commit 7a69c4c

8 files changed

Lines changed: 58 additions & 15 deletions

File tree

.github/README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Emberly is an open source platform for modern file storage, sharing, and identit
4747
- Promo code management with configurable discounts
4848
- User management dashboard
4949
- Application review queue with multi stage triage
50-
- Service status monitoring via Kener integration
50+
- Service status page link ([emberlystat.us](https://emberlystat.us))
5151
- Analytics and usage reporting
5252

5353
## Quick Start
@@ -105,7 +105,6 @@ The application will be available at http://localhost:3000.
105105
**Infrastructure & Services**
106106

107107
- [S3 compatible storage](https://aws.amazon.com/s3/) - File storage
108-
- [Kener](https://kener.ing/) - Status page monitoring
109108
- [Next.js Auth](https://next-auth.js.org/) - Authentication
110109
- [Sentry](https://sentry.io/) - Error tracking
111110

@@ -176,7 +175,6 @@ Get help and connect with the community:
176175

177176
This project is licensed under the GNU Affero General Public License v3 (AGPL-3.0). See the [LICENSE](LICENSE) file for details.
178177

179-
180178
## Code of Conduct
181179

182180
This project adheres to the Contributor Covenant Code of Conduct. By participating, you agree to uphold this code. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for the full text.
@@ -185,7 +183,6 @@ This project adheres to the Contributor Covenant Code of Conduct. By participati
185183

186184
Thank you to all [contributors](https://github.com/EmberlyOSS/Emberly/graphs/contributors) who have helped make Emberly possible. We also appreciate the [open source projects and communities](https://github.com/EmberlyOSS/Emberly/network/dependencies) that make Emberly possible.
187185

188-
189186
<a href="https://github.com/EmberlyOSS/Emberly/graphs/contributors">
190187
<img src="https://contrib.rocks/image?repo=EmberlyOSS/Emberly" alt="Contributors" />
191188
</a>

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The format is based on "Keep a Changelog" and follows [Semantic Versioning](http
2525
### Changed
2626

2727
- **Status Page Integration Removed**
28-
- Removed the Kener / Uptime Kuma dynamic status integration entirely. The polling logic, `/api/status` route, admin settings panel, and integration test handler have all been removed.
28+
- Removed the Kener / Uptime Kuma dynamic status integration entirely. The polling logic, `/api/status` route (now returns 404), admin settings panel, and integration test handler have all been removed.
2929
- `StatusIndicator` in the site footer is now a lightweight static link to [emberlystat.us](https://emberlystat.us) — no external API calls, no runtime failures, no "Status unknown" states.
3030
- `KENER_API_KEY`, `KENER_BASE_URL`, `UPTIME_KUMA_BASE_URL`, and `UPTIME_KUMA_SLUG` environment variables are no longer used and can be removed.
3131

@@ -53,6 +53,13 @@ The format is based on "Keep a Changelog" and follows [Semantic Versioning](http
5353
- **ESLint — Empty interfaces in `react-jsx-compat.d.ts`** — Six `interface X extends Y {}` declarations with no added members replaced with `type X = Y` aliases, satisfying `@typescript-eslint/no-empty-object-type`.
5454
- **ESLint — Unused variables across multiple files** — Removed or prefixed unused imports and variables flagged by `@typescript-eslint/no-unused-vars`: `Copy` in `bucket/page.tsx`, `User` in `blog/page.tsx`, `Share2`/`User` in `blog/[slug]/page.tsx`, `Footer`/`getConfig`/`providedPassword` in `[filename]/page.tsx`, `toast` in `squads/client.tsx`, `codeSent` state in `alpha-migration/page.tsx`, and `userName` parameter in `dashboard/client.tsx`.
5555
- **ESLint — `let``const` in `theme-initializer.tsx`**`cssVariables` was declared with `let` but never reassigned; declaration moved to the single assignment site as `const`.
56+
- **Integration test fetch timeouts**`testStripe`, `testResend`, `testCloudflare`, `testDiscord` (bot + webhook), and `testGitHub` had no request timeout, allowing the admin integration-test endpoint to hang indefinitely if a provider didn't respond. All now use `AbortSignal.timeout(8000)`, consistent with the existing Vultr handler.
57+
- **Quarantine failure logging**`Promise.allSettled` results in the VirusTotal quarantine path were silently discarded. Storage delete or DB update failures now log an error with the file ID so they can be investigated.
58+
- **Session cache `emailVerified` backfill** — Redis-cached sessions written before the `emailVerified: boolean` field was added would deserialize with `undefined`, breaking the upload auth contract. A coerce-on-read now sets `false` for any entry missing the field.
59+
- **Legacy `kener`/`uptimeKuma` config keys stripped from admin response** — The integration config schema uses `.passthrough()`, so old DB records containing stale Kener or Uptime Kuma keys would survive `deepMerge` and be returned to SUPERADMIN via `GET /api/settings`. `maskSecretsForAdmin` now explicitly deletes both keys before returning.
60+
- **Empty filename slug fallback** — Filenames composed entirely of non-ASCII/symbol characters would reduce to an empty slug after sanitization, producing broken storage paths. A `nanoid(6)` fallback is now used when the slug is empty.
61+
- **`storageQuotaMB = 0` admin override ignored**`if (!baseQuotaMB)` treated an explicit zero-quota override as "unset", falling through to plan-based quota. Changed to `== null` checks so `0` is honored as a deliberate override.
62+
- **Storage-bucket subscription precedence missing from Stripe re-check path** — After a successful Stripe sync, `getPlanLimits` returned the latest active subscription without first checking for a `storage-bucket-*` subscription. Users with both sub types active could receive non-unlimited limits for that request. The re-check now mirrors the original early-exit logic.
5663

5764
## [2.4.5] - 2026-06-02
5865

app/api/admin/integrations/test/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ async function testStripe(secretKey: string): Promise<TestResult> {
6060
try {
6161
const res = await fetch('https://api.stripe.com/v1/customers?limit=1', {
6262
headers: { Authorization: `Bearer ${secretKey}` },
63+
signal: AbortSignal.timeout(8000),
6364
})
6465
if (res.status === 401)
6566
return {
@@ -84,6 +85,7 @@ async function testResend(apiKey: string): Promise<TestResult> {
8485
try {
8586
const res = await fetch('https://api.resend.com/domains', {
8687
headers: { Authorization: `Bearer ${apiKey}` },
88+
signal: AbortSignal.timeout(8000),
8789
})
8890
if (res.status === 401 || res.status === 403)
8991
return { ok: false, message: 'Invalid API key' }
@@ -125,6 +127,7 @@ async function testCloudflare(
125127
Authorization: `Bearer ${apiToken}`,
126128
'Content-Type': 'application/json',
127129
},
130+
signal: AbortSignal.timeout(8000),
128131
})
129132
const json = await res.json().catch(() => null)
130133
if (!res.ok || json?.success === false) {
@@ -168,6 +171,7 @@ async function testDiscord(
168171
`https://discord.com/api/v10/guilds/${safeServerId}`,
169172
{
170173
headers: { Authorization: `Bot ${botToken}` },
174+
signal: AbortSignal.timeout(8000),
171175
}
172176
)
173177
if (res.status === 401) return { ok: false, message: 'Invalid bot token' }
@@ -226,7 +230,10 @@ async function testDiscord(
226230
const [, webhookId, webhookToken] = match
227231
const safeWebhookUrl = `https://discord.com/api/webhooks/${encodeURIComponent(webhookId)}/${encodeURIComponent(webhookToken)}`
228232

229-
const res = await fetch(safeWebhookUrl, { method: 'GET' })
233+
const res = await fetch(safeWebhookUrl, {
234+
method: 'GET',
235+
signal: AbortSignal.timeout(8000),
236+
})
230237
if (res.status === 401)
231238
return { ok: false, message: 'Invalid webhook URL' }
232239
if (!res.ok)
@@ -270,6 +277,7 @@ async function testGitHub(pat: string, org?: string): Promise<TestResult> {
270277
Accept: 'application/vnd.github+json',
271278
'X-GitHub-Api-Version': '2022-11-28',
272279
},
280+
signal: AbortSignal.timeout(8000),
273281
})
274282
if (res.status === 401)
275283
return { ok: false, message: 'Invalid personal access token' }

app/api/files/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,22 @@ export async function POST(req: Request) {
279279
permalink: vtResult.permalink,
280280
userId: user.id,
281281
})
282-
await Promise.allSettled([
282+
const results = await Promise.allSettled([
283283
storageProvider!.deleteFile(filePath),
284284
prisma.file.update({
285285
where: { id: fileRecord.id },
286286
data: { visibility: 'PRIVATE', name: '[Quarantined]' },
287287
}),
288288
])
289+
results.forEach((r, i) => {
290+
if (r.status === 'rejected') {
291+
logger.error(
292+
`Quarantine step ${i === 0 ? 'storage delete' : 'db update'} failed`,
293+
r.reason as Error,
294+
{ fileId: fileRecord.id }
295+
)
296+
}
297+
})
289298
}).catch((err) => {
290299
logger.error('Background VirusTotal scan failed', err as Error, {
291300
fileId: fileRecord.id,

app/api/settings/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
} from '@/packages/types/dto/settings'
66

77
import { HTTP_STATUS, apiError, apiResponse } from '@/packages/lib/api/response'
8-
import {
9-
requireAuth,
10-
requireSuperAdmin,
11-
} from '@/packages/lib/auth/api-auth'
8+
import { requireAuth, requireSuperAdmin } from '@/packages/lib/auth/api-auth'
129
import {
1310
EmberlyConfig,
1411
getConfig,
@@ -50,6 +47,9 @@ function maskSecretsForAdmin(config: EmberlyConfig): EmberlyConfig {
5047
if (i.github) {
5148
i.github.pat = ''
5249
}
50+
// Remove any stale legacy integration keys that may still live in DB config
51+
delete i.kener
52+
delete i.uptimeKuma
5353
return c as EmberlyConfig
5454
}
5555

packages/lib/cache/session-cache.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export const sessionCache = {
3939
const redis = await getRedisClient()
4040
const data = await redis.get(redisKeys.userSession(userId))
4141
if (!data) return null
42-
return JSON.parse(data) as CachedUserSession
42+
const parsed = JSON.parse(data) as CachedUserSession
43+
// Coerce emailVerified for older cached entries that pre-date the boolean field
44+
if (typeof parsed.emailVerified !== 'boolean') {
45+
parsed.emailVerified = false
46+
}
47+
return parsed
4348
} catch (error) {
4449
logger.error('Failed to get cached user session', error as Error, {
4550
userId,

packages/lib/files/filename.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export async function getUniqueFilename(
101101
while (s < e && urlSafeName[s] === '-') s++
102102
while (e > s && urlSafeName[e - 1] === '-') e--
103103
urlSafeName = urlSafeName.slice(s, e)
104+
if (!urlSafeName) urlSafeName = nanoid(6)
104105

105106
if (extension) {
106107
urlSafeName += '.' + extension.toLowerCase()

packages/lib/storage/quota.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,23 @@ export async function getPlanLimits(userId: string): Promise<PlanLimits> {
112112
userId,
113113
userRecord.stripeCustomerId
114114
)
115-
// Re-check after sync
115+
// Re-check after sync — storage-bucket subs take precedence (unlimited)
116+
const syncedBucketSub = await prisma.subscription.findFirst({
117+
where: {
118+
userId,
119+
status: 'active',
120+
product: { slug: { startsWith: 'storage-bucket' } },
121+
},
122+
select: { id: true },
123+
})
124+
if (syncedBucketSub) {
125+
return {
126+
storageQuotaGB: null,
127+
uploadSizeCapMB: null,
128+
customDomainsLimit: null,
129+
planName: 'Storage Bucket (Unlimited)',
130+
}
131+
}
116132
const syncedSub = await prisma.subscription.findFirst({
117133
where: { userId, status: 'active' },
118134
include: { product: true },
@@ -252,15 +268,15 @@ export async function getEffectiveQuotaMB(
252268

253269
// Priority: admin override > plan quota + perks > default quota
254270
let baseQuotaMB = user.storageQuotaMB
255-
if (!baseQuotaMB) {
271+
if (baseQuotaMB == null) {
256272
if (planLimits.storageQuotaGB === null) {
257273
// Unlimited plan — use a 100 TB sentinel so arithmetic still works
258274
baseQuotaMB = 100 * 1024 * 1024
259275
} else {
260276
baseQuotaMB = (planLimits.storageQuotaGB + perkStorageBonusGB) * 1024
261277
}
262278
}
263-
if (!baseQuotaMB && defaultQuotaMB) {
279+
if (baseQuotaMB == null && defaultQuotaMB != null) {
264280
baseQuotaMB = defaultQuotaMB
265281
}
266282

0 commit comments

Comments
 (0)