Compare commits
8 Commits
94b6de5970
...
16f827e8f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
16f827e8f0
|
|||
|
947c73dbdc
|
|||
|
2baf10b0cd
|
|||
|
8b081d5ba8
|
|||
|
01f1e1c9e3
|
|||
|
26d10d452f
|
|||
|
8b63eb3538
|
|||
|
587e151fbd
|
@@ -0,0 +1,16 @@
|
|||||||
|
id: no-countall-number
|
||||||
|
snapshots:
|
||||||
|
countAll<number>():
|
||||||
|
fixed: countAll()
|
||||||
|
labels:
|
||||||
|
- source: countAll<number>()
|
||||||
|
style: primary
|
||||||
|
start: 0
|
||||||
|
end: 18
|
||||||
|
eb.fn.countAll<number>().as("count"):
|
||||||
|
fixed: eb.fn.countAll().as("count")
|
||||||
|
labels:
|
||||||
|
- source: eb.fn.countAll<number>()
|
||||||
|
style: primary
|
||||||
|
start: 0
|
||||||
|
end: 24
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
id: no-string-function
|
||||||
|
snapshots:
|
||||||
|
String(123):
|
||||||
|
labels:
|
||||||
|
- source: String(123)
|
||||||
|
style: primary
|
||||||
|
start: 0
|
||||||
|
end: 11
|
||||||
|
String(Date.now()):
|
||||||
|
labels:
|
||||||
|
- source: String(Date.now())
|
||||||
|
style: primary
|
||||||
|
start: 0
|
||||||
|
end: 18
|
||||||
|
String(value):
|
||||||
|
labels:
|
||||||
|
- source: String(value)
|
||||||
|
style: primary
|
||||||
|
start: 0
|
||||||
|
end: 13
|
||||||
@@ -3,7 +3,7 @@ snapshots:
|
|||||||
? |
|
? |
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
: fixed: |
|
: fixed: |
|
||||||
import * as z from "zod"
|
import * as z from "zod";
|
||||||
labels:
|
labels:
|
||||||
- source: import { z } from "zod";
|
- source: import { z } from "zod";
|
||||||
style: primary
|
style: primary
|
||||||
@@ -12,7 +12,7 @@ snapshots:
|
|||||||
? |
|
? |
|
||||||
import { z, ZodError } from "zod";
|
import { z, ZodError } from "zod";
|
||||||
: fixed: |
|
: fixed: |
|
||||||
import * as z from "zod"
|
import * as z from "zod";
|
||||||
labels:
|
labels:
|
||||||
- source: import { z, ZodError } from "zod";
|
- source: import { z, ZodError } from "zod";
|
||||||
style: primary
|
style: primary
|
||||||
|
|||||||
9
.ast-grep/rule-tests/no-countall-number-test.yml
Normal file
9
.ast-grep/rule-tests/no-countall-number-test.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
id: no-countall-number
|
||||||
|
valid:
|
||||||
|
# Plain countAll() is fine
|
||||||
|
- eb.fn.countAll().as("count")
|
||||||
|
# Other type arguments are fine
|
||||||
|
- eb.fn.countAll<string>().as("count")
|
||||||
|
invalid:
|
||||||
|
# countAll<number>() should be flagged
|
||||||
|
- eb.fn.countAll<number>().as("count")
|
||||||
13
.ast-grep/rule-tests/no-string-function-test.yml
Normal file
13
.ast-grep/rule-tests/no-string-function-test.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
id: no-string-function
|
||||||
|
valid:
|
||||||
|
# toString() is fine
|
||||||
|
- value.toString()
|
||||||
|
- (123).toString()
|
||||||
|
- date.toLocaleString()
|
||||||
|
# Other functions named String are fine
|
||||||
|
- myString(value)
|
||||||
|
invalid:
|
||||||
|
# String() function should be flagged
|
||||||
|
- String(value)
|
||||||
|
- String(123)
|
||||||
|
- String(Date.now())
|
||||||
@@ -4,5 +4,5 @@ severity: error
|
|||||||
message: "Don't use countAll<number>() - use countAll() instead. PostgreSQL COUNT returns bigint (string), so the type annotation is misleading."
|
message: "Don't use countAll<number>() - use countAll() instead. PostgreSQL COUNT returns bigint (string), so the type annotation is misleading."
|
||||||
note: "Use Number() to convert the result if you need a number type."
|
note: "Use Number() to convert the result if you need a number type."
|
||||||
rule:
|
rule:
|
||||||
pattern: countAll<number>()
|
pattern: $OBJ.countAll<number>()
|
||||||
fix: countAll()
|
fix: $OBJ.countAll()
|
||||||
|
|||||||
7
.ast-grep/rules/no-string-function.yml
Normal file
7
.ast-grep/rules/no-string-function.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
id: no-string-function
|
||||||
|
language: typescript
|
||||||
|
severity: error
|
||||||
|
message: "Don't use String() - use .toString() or .toLocaleString() instead."
|
||||||
|
note: "String() can have unexpected behavior. Use .toString() for general conversion or .toLocaleString() for locale-aware formatting."
|
||||||
|
rule:
|
||||||
|
pattern: String($VAL)
|
||||||
10
README.md
10
README.md
@@ -26,9 +26,11 @@ A modern publisher dashboard for managing organizations, members, and sites. Bui
|
|||||||
|
|
||||||
### Shared Packages
|
### Shared Packages
|
||||||
- `@reviq/api-contract` - Shared API contract (oRPC)
|
- `@reviq/api-contract` - Shared API contract (oRPC)
|
||||||
|
- `@reviq/common` - Shared utilities for frontend and backend
|
||||||
- `@reviq/db` - Database client and queries
|
- `@reviq/db` - Database client and queries
|
||||||
- `@reviq/db-schema` - Database schema and codegen
|
- `@reviq/db-schema` - Database schema and codegen
|
||||||
- `@reviq/utils` - Shared utilities
|
- `@reviq/frontend-utils` - Frontend-specific utilities
|
||||||
|
- `@reviq/server-utils` - Server/CLI utilities
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -40,10 +42,12 @@ publisher-dashboard/
|
|||||||
│ └── publisher-dashboard/ # SvelteKit frontend
|
│ └── publisher-dashboard/ # SvelteKit frontend
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── api-contract/ # Shared oRPC contract
|
│ ├── api-contract/ # Shared oRPC contract
|
||||||
|
│ ├── common/ # Shared utilities (frontend + backend)
|
||||||
│ ├── db/ # Database client
|
│ ├── db/ # Database client
|
||||||
│ ├── db-schema/ # DB schema & codegen
|
│ ├── db-schema/ # DB schema & codegen
|
||||||
│ ├── testing/ # Test utilities
|
│ ├── frontend-utils/ # Frontend utilities
|
||||||
│ └── utils/ # Shared utilities
|
│ ├── server-utils/ # Server/CLI utilities
|
||||||
|
│ └── testing/ # Test utilities
|
||||||
└── db/ # Database migrations
|
└── db/ # Database migrations
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"@reviq/api-contract": "workspace:*",
|
"@reviq/api-contract": "workspace:*",
|
||||||
"@reviq/db": "workspace:*",
|
"@reviq/db": "workspace:*",
|
||||||
"@reviq/db-schema": "workspace:*",
|
"@reviq/db-schema": "workspace:*",
|
||||||
"@reviq/utils": "workspace:*",
|
"@reviq/server-utils": "workspace:*",
|
||||||
"@scure/base": "^2.0.0",
|
"@scure/base": "^2.0.0",
|
||||||
"@simplewebauthn/server": "^13.2.2",
|
"@simplewebauthn/server": "^13.2.2",
|
||||||
"@simplewebauthn/types": "^12.0.0",
|
"@simplewebauthn/types": "^12.0.0",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
initTestDb,
|
initTestDb,
|
||||||
TEST_RP,
|
TEST_RP,
|
||||||
truncateAllTables,
|
truncateAllTables,
|
||||||
|
uniqueTestId,
|
||||||
withTestTransaction,
|
withTestTransaction,
|
||||||
} from "@reviq/test-helpers";
|
} from "@reviq/test-helpers";
|
||||||
import { router } from "../../router.js";
|
import { router } from "../../router.js";
|
||||||
@@ -84,7 +85,7 @@ async function createSession(
|
|||||||
db: Kysely<Database>,
|
db: Kysely<Database>,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<{ token: string; sessionId: number }> {
|
): Promise<{ token: string; sessionId: number }> {
|
||||||
const token = `test-session-${String(Date.now())}${String(Math.random())}`;
|
const token = `test-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -115,9 +116,7 @@ async function createOrg(
|
|||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
},
|
},
|
||||||
): Promise<{ id: number; slug: string }> {
|
): Promise<{ id: number; slug: string }> {
|
||||||
const slug =
|
const slug = options?.slug ?? `org-${uniqueTestId()}`;
|
||||||
options?.slug ??
|
|
||||||
`org-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`;
|
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.insertInto("orgs")
|
.insertInto("orgs")
|
||||||
@@ -183,7 +182,7 @@ async function createLoginRequest(
|
|||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
},
|
},
|
||||||
): Promise<{ id: number; token: string }> {
|
): Promise<{ id: number; token: string }> {
|
||||||
const token = `login-${String(Date.now())}${String(Math.random())}`;
|
const token = `login-${uniqueTestId()}`;
|
||||||
const expiresAt =
|
const expiresAt =
|
||||||
options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS);
|
options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -212,7 +211,7 @@ async function createOrgInvite(
|
|||||||
email: string,
|
email: string,
|
||||||
invitedBy: number,
|
invitedBy: number,
|
||||||
): Promise<{ id: number }> {
|
): Promise<{ id: number }> {
|
||||||
const token = `invite-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const token = `invite-${uniqueTestId()}`;
|
||||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
@@ -461,7 +460,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("creates passwordless user", async () => {
|
test("creates passwordless user", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -492,7 +491,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("creates user with name", async () => {
|
test("creates user with name", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -519,7 +518,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("creates user and adds to organization as member", async () => {
|
test("creates user and adds to organization as member", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -554,7 +553,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("creates user and adds to organization with custom role", async () => {
|
test("creates user and adds to organization with custom role", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -588,7 +587,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("normalizes email to lowercase", async () => {
|
test("normalizes email to lowercase", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -615,7 +614,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws CONFLICT for duplicate email", async () => {
|
test("throws CONFLICT for duplicate email", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -637,7 +636,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws NOT_FOUND for non-existent org", async () => {
|
test("throws NOT_FOUND for non-existent org", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1060,7 +1059,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("creates organization with owner", async () => {
|
test("creates organization with owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1109,7 +1108,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("normalizes owner email to lowercase", async () => {
|
test("normalizes owner email to lowercase", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1135,7 +1134,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws NOT_FOUND for non-existent owner", async () => {
|
test("throws NOT_FOUND for non-existent owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1160,7 +1159,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws CONFLICT for duplicate slug", async () => {
|
test("throws CONFLICT for duplicate slug", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1284,7 +1283,7 @@ describeE2E("admin", () => {
|
|||||||
await createOrg(db, {
|
await createOrg(db, {
|
||||||
slug: "test-org",
|
slug: "test-org",
|
||||||
displayName: "Old",
|
displayName: "Old",
|
||||||
logoUrl: null,
|
logoUrl: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { token: sessionToken } = await createSession(db, admin.id);
|
const { token: sessionToken } = await createSession(db, admin.id);
|
||||||
@@ -1379,7 +1378,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("deletes organization and related records", async () => {
|
test("deletes organization and related records", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1444,7 +1443,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws NOT_FOUND for non-existent organization", async () => {
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1564,7 +1563,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("adds site to organization", async () => {
|
test("adds site to organization", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1596,7 +1595,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws NOT_FOUND for non-existent organization", async () => {
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1617,7 +1616,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws CONFLICT for duplicate domain", async () => {
|
test("throws CONFLICT for duplicate domain", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1640,7 +1639,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
test("throws CONFLICT for domain in another organization", async () => {
|
test("throws CONFLICT for domain in another organization", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1793,7 +1792,7 @@ describeE2E("admin", () => {
|
|||||||
// Verify login request was completed
|
// Verify login request was completed
|
||||||
const request = await db
|
const request = await db
|
||||||
.selectFrom("login_requests")
|
.selectFrom("login_requests")
|
||||||
.where("id", "=", String(loginRequest.id))
|
.where("id", "=", loginRequest.id.toString())
|
||||||
.select(["completed_at"])
|
.select(["completed_at"])
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
@@ -1825,7 +1824,7 @@ describeE2E("admin", () => {
|
|||||||
|
|
||||||
const request = await db
|
const request = await db
|
||||||
.selectFrom("login_requests")
|
.selectFrom("login_requests")
|
||||||
.where("id", "=", String(loginRequest.id))
|
.where("id", "=", loginRequest.id.toString())
|
||||||
.select(["completed_at"])
|
.select(["completed_at"])
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
getSharedDb,
|
getSharedDb,
|
||||||
initTestDb,
|
initTestDb,
|
||||||
TEST_RP,
|
TEST_RP,
|
||||||
|
uniqueTestId,
|
||||||
withTestTransaction,
|
withTestTransaction,
|
||||||
} from "@reviq/test-helpers";
|
} from "@reviq/test-helpers";
|
||||||
import { VirtualAuthenticator } from "@reviq/virtual-authenticator";
|
import { VirtualAuthenticator } from "@reviq/virtual-authenticator";
|
||||||
@@ -146,7 +147,7 @@ async function createSession(
|
|||||||
userId: number,
|
userId: number,
|
||||||
options?: { deviceId?: bigint },
|
options?: { deviceId?: bigint },
|
||||||
): Promise<{ token: string; sessionId: number }> {
|
): Promise<{ token: string; sessionId: number }> {
|
||||||
const token = `test-session-${String(Date.now())}${String(Math.random())}`;
|
const token = `test-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ async function createSession(
|
|||||||
.insertInto("sessions")
|
.insertInto("sessions")
|
||||||
.values({
|
.values({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
device_id: options?.deviceId ? String(options.deviceId) : null,
|
device_id: options?.deviceId ? options.deviceId.toString() : null,
|
||||||
token_hash: tokenHashValue,
|
token_hash: tokenHashValue,
|
||||||
trusted_mode: false,
|
trusted_mode: false,
|
||||||
expires_at: expiresAt,
|
expires_at: expiresAt,
|
||||||
@@ -178,7 +179,7 @@ async function createLoginRequest(
|
|||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
},
|
},
|
||||||
): Promise<{ token: string; id: number }> {
|
): Promise<{ token: string; id: number }> {
|
||||||
const token = `login_test-${String(Date.now())}${String(Math.random())}`;
|
const token = `login_test-${uniqueTestId()}`;
|
||||||
const expiresAt =
|
const expiresAt =
|
||||||
options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS);
|
options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -228,7 +229,7 @@ async function createEmailVerification(
|
|||||||
userId: number,
|
userId: number,
|
||||||
options?: { expiresAt?: Date },
|
options?: { expiresAt?: Date },
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const token = `verify-${String(Date.now())}${String(Math.random())}`;
|
const token = `verify-${uniqueTestId()}`;
|
||||||
const expiresAt =
|
const expiresAt =
|
||||||
options?.expiresAt ?? new Date(Date.now() + 24 * 60 * 60 * 1000);
|
options?.expiresAt ?? new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
@@ -252,7 +253,7 @@ async function createPasswordReset(
|
|||||||
userId: number,
|
userId: number,
|
||||||
options?: { expiresAt?: Date; usedAt?: Date | null },
|
options?: { expiresAt?: Date; usedAt?: Date | null },
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const token = `reset-${String(Date.now())}${String(Math.random())}`;
|
const token = `reset-${uniqueTestId()}`;
|
||||||
const expiresAt = options?.expiresAt ?? new Date(Date.now() + 60 * 60 * 1000);
|
const expiresAt = options?.expiresAt ?? new Date(Date.now() + 60 * 60 * 1000);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@@ -457,7 +458,7 @@ describeE2E("auth", () => {
|
|||||||
const challenges = await db
|
const challenges = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
expect(challenges.length).toBe(0);
|
expect(challenges.length).toBe(0);
|
||||||
});
|
});
|
||||||
@@ -483,7 +484,7 @@ describeE2E("auth", () => {
|
|||||||
await db
|
await db
|
||||||
.updateTable("webauthn_challenges")
|
.updateTable("webauthn_challenges")
|
||||||
.set({ created_at: new Date(Date.now() - 20 * 60 * 1000) }) // 20 minutes ago
|
.set({ created_at: new Date(Date.now() - 20 * 60 * 1000) }) // 20 minutes ago
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// Step 4: Try to signup with expired challenge
|
// Step 4: Try to signup with expired challenge
|
||||||
@@ -540,7 +541,7 @@ describeE2E("auth", () => {
|
|||||||
const challenges = await db
|
const challenges = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
expect(challenges.length).toBe(0);
|
expect(challenges.length).toBe(0);
|
||||||
});
|
});
|
||||||
@@ -1072,7 +1073,7 @@ describeE2E("auth", () => {
|
|||||||
const loginRequest = await db
|
const loginRequest = await db
|
||||||
.selectFrom("login_requests")
|
.selectFrom("login_requests")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where("id", "=", String(loginRequestId))
|
.where("id", "=", loginRequestId.toString())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
expect(loginRequest).toBeUndefined();
|
expect(loginRequest).toBeUndefined();
|
||||||
|
|
||||||
@@ -1152,7 +1153,7 @@ describeE2E("auth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create login request without device fingerprint
|
// Create login request without device fingerprint
|
||||||
const token = `login_test-${String(Date.now())}`;
|
const token = `login_test-${uniqueTestId()}`;
|
||||||
await db
|
await db
|
||||||
.insertInto("login_requests")
|
.insertInto("login_requests")
|
||||||
.values({
|
.values({
|
||||||
@@ -1644,7 +1645,7 @@ describeE2E("auth", () => {
|
|||||||
const session = await db
|
const session = await db
|
||||||
.selectFrom("sessions")
|
.selectFrom("sessions")
|
||||||
.select(["revoked_at"])
|
.select(["revoked_at"])
|
||||||
.where("id", "=", String(sessionId))
|
.where("id", "=", sessionId.toString())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
expect(session?.revoked_at).not.toBeNull();
|
expect(session?.revoked_at).not.toBeNull();
|
||||||
@@ -1981,7 +1982,7 @@ describeE2E("auth", () => {
|
|||||||
// Clean up registration session
|
// Clean up registration session
|
||||||
await db
|
await db
|
||||||
.deleteFrom("sessions")
|
.deleteFrom("sessions")
|
||||||
.where("id", "=", String(regSessionId))
|
.where("id", "=", regSessionId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// Step 1: Create login request
|
// Step 1: Create login request
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
getSharedDb,
|
getSharedDb,
|
||||||
initTestDb,
|
initTestDb,
|
||||||
TEST_RP,
|
TEST_RP,
|
||||||
|
uniqueTestId,
|
||||||
withTestTransaction,
|
withTestTransaction,
|
||||||
} from "@reviq/test-helpers";
|
} from "@reviq/test-helpers";
|
||||||
import { router } from "../../router.js";
|
import { router } from "../../router.js";
|
||||||
@@ -92,7 +93,7 @@ async function createSession(
|
|||||||
userId: number,
|
userId: number,
|
||||||
options?: { ipAddress?: string; userAgent?: string },
|
options?: { ipAddress?: string; userAgent?: string },
|
||||||
): Promise<{ token: string; sessionId: number }> {
|
): Promise<{ token: string; sessionId: number }> {
|
||||||
const token = `test-session-${String(Date.now())}${String(Math.random())}`;
|
const token = `test-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -125,9 +126,7 @@ async function createDevice(
|
|||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
},
|
},
|
||||||
): Promise<{ fingerprint: string; deviceId: number }> {
|
): Promise<{ fingerprint: string; deviceId: number }> {
|
||||||
const fingerprint =
|
const fingerprint = options?.fingerprint ?? `test-fp-${uniqueTestId()}`;
|
||||||
options?.fingerprint ??
|
|
||||||
`test-fp-${String(Date.now())}${String(Math.random())}`;
|
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.insertInto("user_devices")
|
.insertInto("user_devices")
|
||||||
@@ -153,7 +152,7 @@ async function createApiToken(
|
|||||||
db: Kysely<Database>,
|
db: Kysely<Database>,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<{ token: string; name: string }> {
|
): Promise<{ token: string; name: string }> {
|
||||||
const token = `test-api-token-${String(Date.now())}${String(Math.random())}`;
|
const token = `test-api-token-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
const expiresAt = new Date(Date.now() + API_TOKEN_EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + API_TOKEN_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -224,7 +223,7 @@ describeE2E("me", () => {
|
|||||||
const user = await createTestUser(db, { email: "expired@example.com" });
|
const user = await createTestUser(db, { email: "expired@example.com" });
|
||||||
|
|
||||||
// Create an expired session
|
// Create an expired session
|
||||||
const token = `expired-session-${String(Date.now())}`;
|
const token = `expired-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
await db
|
await db
|
||||||
.insertInto("sessions")
|
.insertInto("sessions")
|
||||||
@@ -249,7 +248,7 @@ describeE2E("me", () => {
|
|||||||
const user = await createTestUser(db, { email: "revoked@example.com" });
|
const user = await createTestUser(db, { email: "revoked@example.com" });
|
||||||
|
|
||||||
// Create a revoked session
|
// Create a revoked session
|
||||||
const token = `revoked-session-${String(Date.now())}`;
|
const token = `revoked-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
await db
|
await db
|
||||||
.insertInto("sessions")
|
.insertInto("sessions")
|
||||||
@@ -925,7 +924,7 @@ describeE2E("me", () => {
|
|||||||
country: "US",
|
country: "US",
|
||||||
trusted_mode: true,
|
trusted_mode: true,
|
||||||
})
|
})
|
||||||
.where("id", "=", String(sessionId))
|
.where("id", "=", sessionId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const context = createAPIContext(db, { sessionToken });
|
const context = createAPIContext(db, { sessionToken });
|
||||||
@@ -968,7 +967,7 @@ describeE2E("me", () => {
|
|||||||
const session = await db
|
const session = await db
|
||||||
.selectFrom("sessions")
|
.selectFrom("sessions")
|
||||||
.select(["revoked_at"])
|
.select(["revoked_at"])
|
||||||
.where("id", "=", String(sessionId2))
|
.where("id", "=", sessionId2.toString())
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
expect(session.revoked_at).not.toBeNull();
|
expect(session.revoked_at).not.toBeNull();
|
||||||
@@ -1021,7 +1020,7 @@ describeE2E("me", () => {
|
|||||||
await db
|
await db
|
||||||
.updateTable("sessions")
|
.updateTable("sessions")
|
||||||
.set({ revoked_at: new Date() })
|
.set({ revoked_at: new Date() })
|
||||||
.where("id", "=", String(sessionId2))
|
.where("id", "=", sessionId2.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const context = createAPIContext(db, { sessionToken: sessionToken1 });
|
const context = createAPIContext(db, { sessionToken: sessionToken1 });
|
||||||
@@ -1080,7 +1079,7 @@ describeE2E("me", () => {
|
|||||||
const currentSession = await db
|
const currentSession = await db
|
||||||
.selectFrom("sessions")
|
.selectFrom("sessions")
|
||||||
.select(["revoked_at"])
|
.select(["revoked_at"])
|
||||||
.where("id", "=", String(id1))
|
.where("id", "=", id1.toString())
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
expect(currentSession.revoked_at).toBeNull();
|
expect(currentSession.revoked_at).toBeNull();
|
||||||
|
|
||||||
@@ -1088,7 +1087,7 @@ describeE2E("me", () => {
|
|||||||
const otherSessions = await db
|
const otherSessions = await db
|
||||||
.selectFrom("sessions")
|
.selectFrom("sessions")
|
||||||
.select(["id", "revoked_at"])
|
.select(["id", "revoked_at"])
|
||||||
.where("id", "in", [String(id2), String(id3)])
|
.where("id", "in", [id2.toString(), id3.toString()])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
for (const session of otherSessions) {
|
for (const session of otherSessions) {
|
||||||
@@ -1116,7 +1115,7 @@ describeE2E("me", () => {
|
|||||||
const session = await db
|
const session = await db
|
||||||
.selectFrom("sessions")
|
.selectFrom("sessions")
|
||||||
.select(["revoked_at"])
|
.select(["revoked_at"])
|
||||||
.where("id", "=", String(sessionId))
|
.where("id", "=", sessionId.toString())
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
expect(session.revoked_at).toBeNull();
|
expect(session.revoked_at).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -1147,7 +1146,7 @@ describeE2E("me", () => {
|
|||||||
region: "NY",
|
region: "NY",
|
||||||
country: "US",
|
country: "US",
|
||||||
})
|
})
|
||||||
.where("id", "=", String(deviceId))
|
.where("id", "=", deviceId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const { token: sessionToken } = await createSession(db, user.id);
|
const { token: sessionToken } = await createSession(db, user.id);
|
||||||
@@ -1256,7 +1255,7 @@ describeE2E("me", () => {
|
|||||||
const device = await db
|
const device = await db
|
||||||
.selectFrom("user_devices")
|
.selectFrom("user_devices")
|
||||||
.select(["is_trusted", "name"])
|
.select(["is_trusted", "name"])
|
||||||
.where("id", "=", String(deviceId))
|
.where("id", "=", deviceId.toString())
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
expect(device.is_trusted).toBe(true);
|
expect(device.is_trusted).toBe(true);
|
||||||
@@ -1401,7 +1400,7 @@ describeE2E("me", () => {
|
|||||||
const device = await db
|
const device = await db
|
||||||
.selectFrom("user_devices")
|
.selectFrom("user_devices")
|
||||||
.select(["is_trusted"])
|
.select(["is_trusted"])
|
||||||
.where("id", "=", String(deviceId))
|
.where("id", "=", deviceId.toString())
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
expect(device.is_trusted).toBe(false);
|
expect(device.is_trusted).toBe(false);
|
||||||
@@ -1501,7 +1500,7 @@ async function createTrustedSession(
|
|||||||
db: Kysely<Database>,
|
db: Kysely<Database>,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<{ token: string; sessionId: number }> {
|
): Promise<{ token: string; sessionId: number }> {
|
||||||
const token = `test-session-${String(Date.now())}${String(Math.random())}`;
|
const token = `test-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -1568,7 +1567,7 @@ async function createOrgInvite(
|
|||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
},
|
},
|
||||||
): Promise<{ id: number }> {
|
): Promise<{ id: number }> {
|
||||||
const token = `invite-token-${String(Date.now())}-${Math.random().toString(36).slice(2)}`;
|
const token = `invite-token-${uniqueTestId()}-${Math.random().toString(36).slice(2)}`;
|
||||||
const result = await db
|
const result = await db
|
||||||
.insertInto("org_invites")
|
.insertInto("org_invites")
|
||||||
.values({
|
.values({
|
||||||
@@ -1693,7 +1692,7 @@ describeE2E("me.apiTokens and me.invites", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(tokens).toHaveLength(1);
|
expect(tokens).toHaveLength(1);
|
||||||
expect(tokens[0].name).toBe("User1 Token");
|
expect(tokens[0]?.name).toBe("User1 Token");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1727,7 +1726,7 @@ describeE2E("me.apiTokens and me.invites", () => {
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
expect(tokens).toHaveLength(1);
|
expect(tokens).toHaveLength(1);
|
||||||
expect(tokens[0].name).toBe("My New Token");
|
expect(tokens[0]?.name).toBe("My New Token");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1937,10 +1936,10 @@ describeE2E("me.apiTokens and me.invites", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(invites).toHaveLength(1);
|
expect(invites).toHaveLength(1);
|
||||||
expect(invites[0].org.slug).toBe("invite-org");
|
expect(invites[0]?.org.slug).toBe("invite-org");
|
||||||
expect(invites[0].org.displayName).toBe("Invite Org");
|
expect(invites[0]?.org.displayName).toBe("Invite Org");
|
||||||
expect(invites[0].role).toBe("admin");
|
expect(invites[0]?.role).toBe("admin");
|
||||||
expect(invites[0].invitedBy).toBe("Inviter Person");
|
expect(invites[0]?.invitedBy).toBe("Inviter Person");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2086,7 +2085,7 @@ describeE2E("me.apiTokens and me.invites", () => {
|
|||||||
describe("me.invites.accept", () => {
|
describe("me.invites.accept", () => {
|
||||||
test("accepts invite and adds user to org", async () => {
|
test("accepts invite and adds user to org", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const inviter = await createTestUser(db, {
|
const inviter = await createTestUser(db, {
|
||||||
email: `inviter-accept-${uniqueId}@example.com`,
|
email: `inviter-accept-${uniqueId}@example.com`,
|
||||||
@@ -2188,7 +2187,7 @@ describeE2E("me.apiTokens and me.invites", () => {
|
|||||||
|
|
||||||
test("returns error if already a member", async () => {
|
test("returns error if already a member", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const inviter = await createTestUser(db, {
|
const inviter = await createTestUser(db, {
|
||||||
email: `inviter-already-${uniqueId}@example.com`,
|
email: `inviter-already-${uniqueId}@example.com`,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
getSharedDb,
|
getSharedDb,
|
||||||
initTestDb,
|
initTestDb,
|
||||||
TEST_RP,
|
TEST_RP,
|
||||||
|
uniqueTestId,
|
||||||
withTestTransaction,
|
withTestTransaction,
|
||||||
} from "@reviq/test-helpers";
|
} from "@reviq/test-helpers";
|
||||||
import { router } from "../../router.js";
|
import { router } from "../../router.js";
|
||||||
@@ -68,7 +69,7 @@ async function createSession(
|
|||||||
userId: number,
|
userId: number,
|
||||||
options?: { trustedMode?: boolean },
|
options?: { trustedMode?: boolean },
|
||||||
): Promise<{ token: string; sessionId: number }> {
|
): Promise<{ token: string; sessionId: number }> {
|
||||||
const token = `test-session-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const token = `test-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -166,9 +167,7 @@ async function createOrgInvite(
|
|||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
},
|
},
|
||||||
): Promise<{ id: number; token: string }> {
|
): Promise<{ id: number; token: string }> {
|
||||||
const token =
|
const token = options?.token ?? `invite-${uniqueTestId()}`;
|
||||||
options?.token ??
|
|
||||||
`invite-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
const expiresAt =
|
const expiresAt =
|
||||||
options?.expiresAt ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
options?.expiresAt ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
@@ -319,7 +318,7 @@ describeE2E("orgs", () => {
|
|||||||
describe("orgs.create", () => {
|
describe("orgs.create", () => {
|
||||||
test("creates org and makes user owner", async () => {
|
test("creates org and makes user owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const user = await createTestUser(db, {
|
const user = await createTestUser(db, {
|
||||||
email: `user-${uniqueId}@example.com`,
|
email: `user-${uniqueId}@example.com`,
|
||||||
@@ -349,7 +348,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("rejects duplicate slug", async () => {
|
test("rejects duplicate slug", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const user = await createTestUser(db, {
|
const user = await createTestUser(db, {
|
||||||
email: `user-${uniqueId}@example.com`,
|
email: `user-${uniqueId}@example.com`,
|
||||||
@@ -532,7 +531,7 @@ describeE2E("orgs", () => {
|
|||||||
describe("orgs.delete", () => {
|
describe("orgs.delete", () => {
|
||||||
test("deletes org when user is owner", async () => {
|
test("deletes org when user is owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const user = await createTestUser(db, {
|
const user = await createTestUser(db, {
|
||||||
email: `user-${uniqueId}@example.com`,
|
email: `user-${uniqueId}@example.com`,
|
||||||
@@ -581,7 +580,7 @@ describeE2E("orgs", () => {
|
|||||||
describe("orgs.leave", () => {
|
describe("orgs.leave", () => {
|
||||||
test("allows member to leave org", async () => {
|
test("allows member to leave org", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -614,7 +613,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("allows owner to leave when there are other owners", async () => {
|
test("allows owner to leave when there are other owners", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner1 = await createTestUser(db, {
|
const owner1 = await createTestUser(db, {
|
||||||
email: `owner1-${uniqueId}@example.com`,
|
email: `owner1-${uniqueId}@example.com`,
|
||||||
@@ -647,7 +646,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("prevents only owner from leaving", async () => {
|
test("prevents only owner from leaving", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -770,7 +769,7 @@ describeE2E("orgs", () => {
|
|||||||
describe("orgs.members.updateRole", () => {
|
describe("orgs.members.updateRole", () => {
|
||||||
test("owner can promote member to admin", async () => {
|
test("owner can promote member to admin", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -803,7 +802,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("owner can promote member to owner", async () => {
|
test("owner can promote member to owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -836,7 +835,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("owner can demote owner to admin when multiple owners exist", async () => {
|
test("owner can demote owner to admin when multiple owners exist", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner1 = await createTestUser(db, {
|
const owner1 = await createTestUser(db, {
|
||||||
email: `owner1-${uniqueId}@example.com`,
|
email: `owner1-${uniqueId}@example.com`,
|
||||||
@@ -869,7 +868,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("prevents demoting the only owner", async () => {
|
test("prevents demoting the only owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -916,7 +915,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("rejects when target member not found", async () => {
|
test("rejects when target member not found", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -942,7 +941,7 @@ describeE2E("orgs", () => {
|
|||||||
describe("orgs.members.remove", () => {
|
describe("orgs.members.remove", () => {
|
||||||
test("owner can remove member", async () => {
|
test("owner can remove member", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -975,7 +974,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("owner can remove admin", async () => {
|
test("owner can remove admin", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1008,7 +1007,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("owner can remove other owner when multiple owners exist", async () => {
|
test("owner can remove other owner when multiple owners exist", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner1 = await createTestUser(db, {
|
const owner1 = await createTestUser(db, {
|
||||||
email: `owner1-${uniqueId}@example.com`,
|
email: `owner1-${uniqueId}@example.com`,
|
||||||
@@ -1041,7 +1040,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("prevents removing the only owner", async () => {
|
test("prevents removing the only owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1065,7 +1064,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("admin can remove member", async () => {
|
test("admin can remove member", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1102,7 +1101,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("admin cannot remove owner", async () => {
|
test("admin cannot remove owner", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1130,7 +1129,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("admin cannot remove other admin", async () => {
|
test("admin cannot remove other admin", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1162,7 +1161,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("member cannot remove anyone", async () => {
|
test("member cannot remove anyone", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1192,7 +1191,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("rejects when target member not found", async () => {
|
test("rejects when target member not found", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1297,7 +1296,7 @@ describeE2E("orgs", () => {
|
|||||||
describe("orgs.invites.create", () => {
|
describe("orgs.invites.create", () => {
|
||||||
test("admin can create member invite", async () => {
|
test("admin can create member invite", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1331,7 +1330,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("admin can create admin invite", async () => {
|
test("admin can create admin invite", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const admin = await createTestUser(db, {
|
const admin = await createTestUser(db, {
|
||||||
email: `admin-${uniqueId}@example.com`,
|
email: `admin-${uniqueId}@example.com`,
|
||||||
@@ -1385,7 +1384,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("owner can create owner invite", async () => {
|
test("owner can create owner invite", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1571,7 +1570,7 @@ describeE2E("orgs", () => {
|
|||||||
describe("orgs.invites.accept", () => {
|
describe("orgs.invites.accept", () => {
|
||||||
test("accepts invite and adds user to org", async () => {
|
test("accepts invite and adds user to org", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
@@ -1669,9 +1668,7 @@ describeE2E("orgs", () => {
|
|||||||
test("rejects when email doesn't match", async () => {
|
test("rejects when email doesn't match", async () => {
|
||||||
await withTestTransaction(getSharedDb(), async (db) => {
|
await withTestTransaction(getSharedDb(), async (db) => {
|
||||||
const owner = await createTestUser(db, { email: "owner@example.com" });
|
const owner = await createTestUser(db, { email: "owner@example.com" });
|
||||||
const _invitee = await createTestUser(db, {
|
await createTestUser(db, { email: "invitee@example.com" });
|
||||||
email: "invitee@example.com",
|
|
||||||
});
|
|
||||||
const wrongUser = await createTestUser(db, {
|
const wrongUser = await createTestUser(db, {
|
||||||
email: "wrong@example.com",
|
email: "wrong@example.com",
|
||||||
});
|
});
|
||||||
@@ -1701,7 +1698,7 @@ describeE2E("orgs", () => {
|
|||||||
|
|
||||||
test("handles already a member gracefully", async () => {
|
test("handles already a member gracefully", async () => {
|
||||||
const db = getSharedDb();
|
const db = getSharedDb();
|
||||||
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
|
const uniqueId = uniqueTestId();
|
||||||
|
|
||||||
const owner = await createTestUser(db, {
|
const owner = await createTestUser(db, {
|
||||||
email: `owner-${uniqueId}@example.com`,
|
email: `owner-${uniqueId}@example.com`,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
initTestDb,
|
initTestDb,
|
||||||
KNOWN_AAGUIDS,
|
KNOWN_AAGUIDS,
|
||||||
TEST_RP,
|
TEST_RP,
|
||||||
|
uniqueTestId,
|
||||||
withTestTransaction,
|
withTestTransaction,
|
||||||
} from "@reviq/test-helpers";
|
} from "@reviq/test-helpers";
|
||||||
import { VirtualAuthenticator } from "@reviq/virtual-authenticator";
|
import { VirtualAuthenticator } from "@reviq/virtual-authenticator";
|
||||||
@@ -60,7 +61,7 @@ async function createSession(
|
|||||||
db: Kysely<Database>,
|
db: Kysely<Database>,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const token = `test-session-${String(Date.now())}${String(Math.random())}`;
|
const token = `test-session-${uniqueTestId()}`;
|
||||||
const tokenHashValue = await hashToken(token);
|
const tokenHashValue = await hashToken(token);
|
||||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ async function createLoginRequest(
|
|||||||
userId: number,
|
userId: number,
|
||||||
email: string,
|
email: string,
|
||||||
): Promise<{ id: number; token: string }> {
|
): Promise<{ id: number; token: string }> {
|
||||||
const token = `test-login-${String(Date.now())}${String(Math.random())}`;
|
const token = `test-login-${uniqueTestId()}`;
|
||||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
@@ -236,7 +237,7 @@ describeE2E("webauthn", () => {
|
|||||||
const challengeRow = await db
|
const challengeRow = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.select("id")
|
.select("id")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
expect(challengeRow).toBeDefined();
|
expect(challengeRow).toBeDefined();
|
||||||
@@ -382,7 +383,7 @@ describeE2E("webauthn", () => {
|
|||||||
const challengeRow = await db
|
const challengeRow = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.select("id")
|
.select("id")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
expect(challengeRow).toBeUndefined();
|
expect(challengeRow).toBeUndefined();
|
||||||
@@ -585,7 +586,7 @@ describeE2E("webauthn", () => {
|
|||||||
const challengeRow = await db
|
const challengeRow = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.select("id")
|
.select("id")
|
||||||
.where("id", "=", String(authChallengeId))
|
.where("id", "=", authChallengeId.toString())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
expect(challengeRow).toBeUndefined();
|
expect(challengeRow).toBeUndefined();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const getAllowedOrigins = (): string[] => {
|
|||||||
|
|
||||||
// Default to localhost origins for development
|
// Default to localhost origins for development
|
||||||
return [
|
return [
|
||||||
`http://localhost:${String(DEFAULT_PORT)}`,
|
`http://localhost:${DEFAULT_PORT.toString()}`,
|
||||||
"http://localhost:6827",
|
"http://localhost:6827",
|
||||||
"http://localhost:6828",
|
"http://localhost:6828",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Bun.serve({
|
|||||||
if (url.pathname.startsWith("/api/v1/rpc")) {
|
if (url.pathname.startsWith("/api/v1/rpc")) {
|
||||||
// Build context for the request
|
// Build context for the request
|
||||||
const origin =
|
const origin =
|
||||||
request.headers.get("origin") ?? `http://localhost:${String(port)}`;
|
request.headers.get("origin") ?? `http://localhost:${port.toString()}`;
|
||||||
|
|
||||||
// Create response headers for setting cookies
|
// Create response headers for setting cookies
|
||||||
const resHeaders = new Headers();
|
const resHeaders = new Headers();
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export async function signupWithPasskey(
|
|||||||
const challengeRow = await db
|
const challengeRow = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.select("options")
|
.select("options")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.where("created_at", ">", fifteenMinutesAgo)
|
.where("created_at", ">", fifteenMinutesAgo)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export async function signupWithPasskey(
|
|||||||
// Delete the challenge
|
// Delete the challenge
|
||||||
await db
|
await db
|
||||||
.deleteFrom("webauthn_challenges")
|
.deleteFrom("webauthn_challenges")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// Log error for debugging but don't expose to client
|
// Log error for debugging but don't expose to client
|
||||||
@@ -149,7 +149,7 @@ export async function signupWithPasskey(
|
|||||||
// Delete the challenge
|
// Delete the challenge
|
||||||
await db
|
await db
|
||||||
.deleteFrom("webauthn_challenges")
|
.deleteFrom("webauthn_challenges")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
throw new ORPCError("BAD_REQUEST", {
|
throw new ORPCError("BAD_REQUEST", {
|
||||||
@@ -200,7 +200,7 @@ export async function signupWithPasskey(
|
|||||||
// Delete the challenge
|
// Delete the challenge
|
||||||
await trx
|
await trx
|
||||||
.deleteFrom("webauthn_challenges")
|
.deleteFrom("webauthn_challenges")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return { userId: newUserId };
|
return { userId: newUserId };
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const deleteApiToken = os.me.apiTokens.delete
|
|||||||
.handler(async ({ input, context }) => {
|
.handler(async ({ input, context }) => {
|
||||||
const result = await context.db
|
const result = await context.db
|
||||||
.deleteFrom("api_tokens")
|
.deleteFrom("api_tokens")
|
||||||
.where("id", "=", String(input.tokenId))
|
.where("id", "=", input.tokenId.toString())
|
||||||
.where("user_id", "=", context.user.id)
|
.where("user_id", "=", context.user.id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export const untrustDevice = os.me.devices.untrust
|
|||||||
const result = await context.db
|
const result = await context.db
|
||||||
.updateTable("user_devices")
|
.updateTable("user_devices")
|
||||||
.set({ is_trusted: false })
|
.set({ is_trusted: false })
|
||||||
.where("id", "=", String(input.deviceId))
|
.where("id", "=", input.deviceId.toString())
|
||||||
.where("user_id", "=", context.user.id)
|
.where("user_id", "=", context.user.id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const renamePasskey = os.me.passkeys.rename
|
|||||||
const result = await context.db
|
const result = await context.db
|
||||||
.updateTable("passkeys")
|
.updateTable("passkeys")
|
||||||
.set({ name })
|
.set({ name })
|
||||||
.where("id", "=", String(passkeyId))
|
.where("id", "=", passkeyId.toString())
|
||||||
.where("user_id", "=", context.user.id)
|
.where("user_id", "=", context.user.id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const deletePasskey = os.me.passkeys.delete
|
|||||||
|
|
||||||
const result = await trx
|
const result = await trx
|
||||||
.deleteFrom("passkeys")
|
.deleteFrom("passkeys")
|
||||||
.where("id", "=", String(passkeyId))
|
.where("id", "=", passkeyId.toString())
|
||||||
.where("user_id", "=", context.user.id)
|
.where("user_id", "=", context.user.id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const revokeSession = os.me.sessions.revoke
|
|||||||
const { sessionId } = input;
|
const { sessionId } = input;
|
||||||
|
|
||||||
// Prevent revoking current session (use logout instead)
|
// Prevent revoking current session (use logout instead)
|
||||||
if (String(sessionId) === context.session.id) {
|
if (sessionId.toString() === context.session.id) {
|
||||||
throw new ORPCError("BAD_REQUEST", {
|
throw new ORPCError("BAD_REQUEST", {
|
||||||
message: "Cannot revoke current session. Use logout instead.",
|
message: "Cannot revoke current session. Use logout instead.",
|
||||||
});
|
});
|
||||||
@@ -57,7 +57,7 @@ export const revokeSession = os.me.sessions.revoke
|
|||||||
const result = await context.db
|
const result = await context.db
|
||||||
.updateTable("sessions")
|
.updateTable("sessions")
|
||||||
.set({ revoked_at: new Date() })
|
.set({ revoked_at: new Date() })
|
||||||
.where("id", "=", String(sessionId))
|
.where("id", "=", sessionId.toString())
|
||||||
.where("user_id", "=", context.user.id)
|
.where("user_id", "=", context.user.id)
|
||||||
.where("revoked_at", "is", null)
|
.where("revoked_at", "is", null)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication
|
|||||||
await context.db
|
await context.db
|
||||||
.updateTable("login_requests")
|
.updateTable("login_requests")
|
||||||
.set({ completed_at: new Date() })
|
.set({ completed_at: new Date() })
|
||||||
.where("id", "=", String(context.loginRequestId))
|
.where("id", "=", context.loginRequestId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { generateSecureBase58Token } from "@reviq/utils";
|
import { generateSecureBase58Token } from "@reviq/server-utils";
|
||||||
import { base58 } from "@scure/base";
|
import { base58 } from "@scure/base";
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
hashPassword as hashPasswordUtil,
|
hashPassword as hashPasswordUtil,
|
||||||
verifyPassword as verifyPasswordUtil,
|
verifyPassword as verifyPasswordUtil,
|
||||||
} from "@reviq/utils";
|
} from "@reviq/server-utils";
|
||||||
import zxcvbn from "zxcvbn";
|
import zxcvbn from "zxcvbn";
|
||||||
|
|
||||||
export interface PasswordValidationResult {
|
export interface PasswordValidationResult {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export const verifyRegistration = async (
|
|||||||
const challengeRow = await db
|
const challengeRow = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.select("options")
|
.select("options")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (!challengeRow) {
|
if (!challengeRow) {
|
||||||
@@ -189,7 +189,7 @@ export const verifyRegistration = async (
|
|||||||
// Always delete the challenge
|
// Always delete the challenge
|
||||||
await db
|
await db
|
||||||
.deleteFrom("webauthn_challenges")
|
.deleteFrom("webauthn_challenges")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ export const verifyAuthentication = async (
|
|||||||
const challengeRow = await db
|
const challengeRow = await db
|
||||||
.selectFrom("webauthn_challenges")
|
.selectFrom("webauthn_challenges")
|
||||||
.select("options")
|
.select("options")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (!challengeRow) {
|
if (!challengeRow) {
|
||||||
@@ -321,7 +321,7 @@ export const verifyAuthentication = async (
|
|||||||
counter: verification.authenticationInfo.newCounter.toString(),
|
counter: verification.authenticationInfo.newCounter.toString(),
|
||||||
last_used_at: new Date(),
|
last_used_at: new Date(),
|
||||||
})
|
})
|
||||||
.where("id", "=", String(passkey.id))
|
.where("id", "=", passkey.id.toString())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -329,7 +329,7 @@ export const verifyAuthentication = async (
|
|||||||
// Always delete the challenge
|
// Always delete the challenge
|
||||||
await db
|
await db
|
||||||
.deleteFrom("webauthn_challenges")
|
.deleteFrom("webauthn_challenges")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", challengeId.toString())
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,5 @@
|
|||||||
"isolatedDeclarations": false,
|
"isolatedDeclarations": false,
|
||||||
"composite": false
|
"composite": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"]
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { LocalContext } from "../../context.js";
|
|||||||
import { ORPCError } from "@orpc/client";
|
import { ORPCError } from "@orpc/client";
|
||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
|
|
||||||
interface CompleteLoginFlags {
|
interface CompleteLoginFlags {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -21,12 +22,10 @@ async function completeLogin(
|
|||||||
console.log(`Completed login request for: ${flags.email}`);
|
console.log(`Completed login request for: ${flags.email}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ORPCError) {
|
if (error instanceof ORPCError) {
|
||||||
console.error(`Error [${String(error.code)}]:`, error.message);
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- ORPCError.code is typed as any
|
||||||
|
console.error(`Error [${error.code}]:`, error.message);
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error("Error:", formatError(error));
|
||||||
"Error:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { LocalContext } from "../../context.js";
|
|||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
import { readConfig, writeConfig } from "../../utils/config.js";
|
import { readConfig, writeConfig } from "../../utils/config.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
|
|
||||||
interface LoginFlags {
|
interface LoginFlags {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -47,10 +48,7 @@ async function login(this: LocalContext, flags: LoginFlags): Promise<void> {
|
|||||||
console.log(`Logged in as ${authStatus.user.email}`);
|
console.log(`Logged in as ${authStatus.user.email}`);
|
||||||
console.log("Credentials saved to ~/.config/reviq/credentials.json");
|
console.log("Credentials saved to ~/.config/reviq/credentials.json");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error("Login failed:", formatError(error));
|
||||||
"Login failed:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
console.log("\nMake sure your API token is valid.");
|
console.log("\nMake sure your API token is valid.");
|
||||||
console.log("You can create a new token at: /account/api-tokens");
|
console.log("You can create a new token at: /account/api-tokens");
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { LocalContext } from "../../context.js";
|
|||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
import { getConfigPath, readConfig } from "../../utils/config.js";
|
import { getConfigPath, readConfig } from "../../utils/config.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
import { TOKEN_PREFIX } from "../../utils/token.js";
|
import { TOKEN_PREFIX } from "../../utils/token.js";
|
||||||
|
|
||||||
function formatDate(date: Date): string {
|
function formatDate(date: Date): string {
|
||||||
@@ -14,19 +15,19 @@ function formatRelativeTime(date: Date): string {
|
|||||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (diffDays < 0) {
|
if (diffDays < 0) {
|
||||||
return `${String(Math.abs(diffDays))} days ago`;
|
return `${Math.abs(diffDays).toLocaleString()} days ago`;
|
||||||
}
|
}
|
||||||
if (diffDays === 0) {
|
if (diffDays === 0) {
|
||||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
if (diffHours <= 0) {
|
if (diffHours <= 0) {
|
||||||
return "expired";
|
return "expired";
|
||||||
}
|
}
|
||||||
return `in ${String(diffHours)} hours`;
|
return `in ${diffHours.toLocaleString()} hours`;
|
||||||
}
|
}
|
||||||
if (diffDays === 1) {
|
if (diffDays === 1) {
|
||||||
return "tomorrow";
|
return "tomorrow";
|
||||||
}
|
}
|
||||||
return `in ${String(diffDays)} days`;
|
return `in ${diffDays.toLocaleString()} days`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function status(this: LocalContext): Promise<void> {
|
async function status(this: LocalContext): Promise<void> {
|
||||||
@@ -96,9 +97,7 @@ async function status(this: LocalContext): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(
|
console.log(` Error: ${formatError(error)}`);
|
||||||
` Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
);
|
|
||||||
console.log(
|
console.log(
|
||||||
"\n Unable to connect to API. Local credentials may be invalid.",
|
"\n Unable to connect to API. Local credentials may be invalid.",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { LocalContext } from "../context.js";
|
|||||||
import { createDb, executeBootstrap } from "@reviq/db";
|
import { createDb, executeBootstrap } from "@reviq/db";
|
||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { writeConfig } from "../utils/config.js";
|
import { writeConfig } from "../utils/config.js";
|
||||||
|
import { formatError } from "../utils/format-error.js";
|
||||||
|
|
||||||
interface BootstrapFlags {
|
interface BootstrapFlags {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -47,10 +48,7 @@ async function bootstrap(
|
|||||||
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error("Error:", formatError(error));
|
||||||
"Error:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LocalContext } from "../../context.js";
|
import type { LocalContext } from "../../context.js";
|
||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
|
|
||||||
interface AddSiteFlags {
|
interface AddSiteFlags {
|
||||||
org: string;
|
org: string;
|
||||||
@@ -18,10 +19,7 @@ async function addSite(this: LocalContext, flags: AddSiteFlags): Promise<void> {
|
|||||||
|
|
||||||
console.log(`Added site ${flags.domain} to org ${flags.org}`);
|
console.log(`Added site ${flags.domain} to org ${flags.org}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error("Error:", formatError(error));
|
||||||
"Error:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LocalContext } from "../../context.js";
|
import type { LocalContext } from "../../context.js";
|
||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
|
|
||||||
interface CreateOrgFlags {
|
interface CreateOrgFlags {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -24,10 +25,7 @@ async function create(
|
|||||||
console.log(`Created org: ${result.slug}`);
|
console.log(`Created org: ${result.slug}`);
|
||||||
console.log(`Owner: ${flags.owner}`);
|
console.log(`Owner: ${flags.owner}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error("Error:", formatError(error));
|
||||||
"Error:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LocalContext } from "../../context.js";
|
import type { LocalContext } from "../../context.js";
|
||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
|
|
||||||
async function list(this: LocalContext): Promise<void> {
|
async function list(this: LocalContext): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -23,12 +24,9 @@ async function list(this: LocalContext): Promise<void> {
|
|||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Total: ${String(orgs.length)} organization(s)`);
|
console.log(`Total: ${orgs.length.toLocaleString()} organization(s)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error("Error:", formatError(error));
|
||||||
"Error:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LocalContext } from "../../context.js";
|
import type { LocalContext } from "../../context.js";
|
||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
|
|
||||||
interface ConfirmEmailFlags {
|
interface ConfirmEmailFlags {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -19,10 +20,7 @@ async function confirmEmail(
|
|||||||
|
|
||||||
console.log(`Confirmed email for: ${flags.email}`);
|
console.log(`Confirmed email for: ${flags.email}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error("Error:", formatError(error));
|
||||||
"Error:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LocalContext } from "../../context.js";
|
import type { LocalContext } from "../../context.js";
|
||||||
import { buildCommand } from "@stricli/core";
|
import { buildCommand } from "@stricli/core";
|
||||||
import { createApiClient } from "../../utils/api-client.js";
|
import { createApiClient } from "../../utils/api-client.js";
|
||||||
|
import { formatError } from "../../utils/format-error.js";
|
||||||
|
|
||||||
type OrgRole = "owner" | "admin" | "member";
|
type OrgRole = "owner" | "admin" | "member";
|
||||||
|
|
||||||
@@ -45,10 +46,7 @@ async function create(
|
|||||||
console.log(`Added to org: ${flags.org} as ${flags.role ?? "member"}`);
|
console.log(`Added to org: ${flags.org} as ${flags.role ?? "member"}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error("Error:", formatError(error));
|
||||||
"Error:",
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
this.process.exit(1);
|
this.process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
apps/cli/src/utils/format-error.ts
Normal file
14
apps/cli/src/utils/format-error.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Format an unknown error value into a string message.
|
||||||
|
* Handles Error instances, strings, and other types safely.
|
||||||
|
*/
|
||||||
|
export function formatError(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
if (typeof error === "string") {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- intentional unknown coercion
|
||||||
|
return `${error}`;
|
||||||
|
}
|
||||||
@@ -19,6 +19,5 @@
|
|||||||
"isolatedDeclarations": false,
|
"isolatedDeclarations": false,
|
||||||
"composite": false
|
"composite": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"]
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
bun.lock
32
bun.lock
@@ -22,7 +22,7 @@
|
|||||||
"@reviq/api-contract": "workspace:*",
|
"@reviq/api-contract": "workspace:*",
|
||||||
"@reviq/db": "workspace:*",
|
"@reviq/db": "workspace:*",
|
||||||
"@reviq/db-schema": "workspace:*",
|
"@reviq/db-schema": "workspace:*",
|
||||||
"@reviq/utils": "workspace:*",
|
"@reviq/server-utils": "workspace:*",
|
||||||
"@scure/base": "^2.0.0",
|
"@scure/base": "^2.0.0",
|
||||||
"@simplewebauthn/server": "^13.2.2",
|
"@simplewebauthn/server": "^13.2.2",
|
||||||
"@simplewebauthn/types": "^12.0.0",
|
"@simplewebauthn/types": "^12.0.0",
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@reviq/db-schema": "workspace:*",
|
"@reviq/db-schema": "workspace:*",
|
||||||
"@reviq/utils": "workspace:*",
|
"@reviq/server-utils": "workspace:*",
|
||||||
"@scure/base": "^2.0.0",
|
"@scure/base": "^2.0.0",
|
||||||
"kysely": "^0.28.9",
|
"kysely": "^0.28.9",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
@@ -191,6 +191,18 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/server-utils": {
|
||||||
|
"name": "@reviq/server-utils",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20250529.0",
|
||||||
|
"@macalinao/eslint-config": "catalog:",
|
||||||
|
"@macalinao/tsconfig": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/testing/test-helpers": {
|
"packages/testing/test-helpers": {
|
||||||
"name": "@reviq/test-helpers",
|
"name": "@reviq/test-helpers",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@@ -224,18 +236,6 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/utils": {
|
|
||||||
"name": "@reviq/utils",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"devDependencies": {
|
|
||||||
"@cloudflare/workers-types": "^4.20250529.0",
|
|
||||||
"@macalinao/eslint-config": "catalog:",
|
|
||||||
"@macalinao/tsconfig": "catalog:",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"eslint": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@macalinao/eslint-config": "^7.0.3",
|
"@macalinao/eslint-config": "^7.0.3",
|
||||||
@@ -456,9 +456,9 @@
|
|||||||
|
|
||||||
"@reviq/frontend-utils": ["@reviq/frontend-utils@workspace:packages/frontend-utils"],
|
"@reviq/frontend-utils": ["@reviq/frontend-utils@workspace:packages/frontend-utils"],
|
||||||
|
|
||||||
"@reviq/test-helpers": ["@reviq/test-helpers@workspace:packages/testing/test-helpers"],
|
"@reviq/server-utils": ["@reviq/server-utils@workspace:packages/server-utils"],
|
||||||
|
|
||||||
"@reviq/utils": ["@reviq/utils@workspace:packages/utils"],
|
"@reviq/test-helpers": ["@reviq/test-helpers@workspace:packages/testing/test-helpers"],
|
||||||
|
|
||||||
"@reviq/virtual-authenticator": ["@reviq/virtual-authenticator@workspace:packages/testing/virtual-authenticator"],
|
"@reviq/virtual-authenticator": ["@reviq/virtual-authenticator@workspace:packages/testing/virtual-authenticator"],
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
"build:watch:packages": "turbo watch build --filter=./packages/*",
|
"build:watch:packages": "turbo watch build --filter=./packages/*",
|
||||||
"build:packages": "turbo build --filter=./packages/*",
|
"build:packages": "turbo build --filter=./packages/*",
|
||||||
"lint": "biome check && turbo run lint",
|
"lint": "biome check && ast-grep scan && turbo run lint",
|
||||||
"lint:fix": "biome check --write --unsafe && turbo run lint -- --fix",
|
"lint:fix": "biome check --write --unsafe && ast-grep scan --update-all && turbo run lint -- --fix",
|
||||||
"typecheck": "turbo typecheck",
|
"typecheck": "turbo typecheck",
|
||||||
"clean": "turbo clean",
|
"clean": "turbo clean",
|
||||||
"test": "turbo test",
|
"test": "turbo test",
|
||||||
|
|||||||
@@ -2,6 +2,5 @@
|
|||||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"isolatedDeclarations": false
|
"isolatedDeclarations": false
|
||||||
},
|
}
|
||||||
"exclude": ["**/*.test.ts"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# @reviq/common
|
# @reviq/common
|
||||||
|
|
||||||
Shared utilities for all RevIQ applications. This package contains environment-agnostic code that works in browsers, Node.js, Bun, and other JavaScript runtimes.
|
Shared utilities for frontend and backend. This package contains environment-agnostic code that works in browsers, Node.js, Bun, and Cloudflare Workers.
|
||||||
|
|
||||||
|
Use this package for utilities that need to work in both the publisher dashboard and the API server.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -95,11 +95,11 @@ export function formatRelativeDate(
|
|||||||
return "Yesterday";
|
return "Yesterday";
|
||||||
}
|
}
|
||||||
if (diffDays < 7) {
|
if (diffDays < 7) {
|
||||||
return `${String(diffDays)} days ago`;
|
return `${diffDays.toLocaleString()} days ago`;
|
||||||
}
|
}
|
||||||
if (diffDays < 30) {
|
if (diffDays < 30) {
|
||||||
const weeks = Math.floor(diffDays / 7);
|
const weeks = Math.floor(diffDays / 7);
|
||||||
return weeks === 1 ? "1 week ago" : `${String(weeks)} weeks ago`;
|
return weeks === 1 ? "1 week ago" : `${weeks.toLocaleString()} weeks ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For older dates, show the actual date
|
// For older dates, show the actual date
|
||||||
|
|||||||
24
packages/db-schema/README.md
Normal file
24
packages/db-schema/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# @reviq/db-schema
|
||||||
|
|
||||||
|
Database schema types generated from PostgreSQL using kysely-codegen.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Database } from "@reviq/db-schema";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regenerating Types
|
||||||
|
|
||||||
|
When the database schema changes, regenerate the types:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd packages/db-schema generate
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires `DATABASE_URL` to be set and pointing to a database with the current schema.
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
- `Database` - The full database type for use with Kysely
|
||||||
|
- Table types for all database tables (e.g., `Users`, `Orgs`, `Sessions`)
|
||||||
@@ -2,6 +2,5 @@
|
|||||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
}
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
packages/db/README.md
Normal file
33
packages/db/README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# @reviq/db
|
||||||
|
|
||||||
|
Database client and helper functions for the RevIQ platform.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createDb } from "@reviq/db";
|
||||||
|
|
||||||
|
const db = createDb(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
// Use db with Kysely queries
|
||||||
|
const users = await db.selectFrom("users").selectAll().execute();
|
||||||
|
|
||||||
|
// Clean up when done
|
||||||
|
await db.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- `createDb(url)` - Create a Kysely database instance
|
||||||
|
|
||||||
|
### Helper Functions
|
||||||
|
- `executeBootstrap(trx, input)` - Bootstrap a new database with superuser and org
|
||||||
|
- `generateToken()` - Generate an API token
|
||||||
|
- `hashToken(token)` - Hash a token for storage
|
||||||
|
- `parseToken(token)` - Parse and validate a token
|
||||||
|
- `TOKEN_PREFIX` - The `reviq_` prefix for API tokens
|
||||||
|
|
||||||
|
### Types
|
||||||
|
- `Database` - Re-exported from `@reviq/db-schema`
|
||||||
|
- `BootstrapInput` / `BootstrapResult` - Types for bootstrap operation
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@reviq/db-schema": "workspace:*",
|
"@reviq/db-schema": "workspace:*",
|
||||||
"@reviq/utils": "workspace:*",
|
"@reviq/server-utils": "workspace:*",
|
||||||
"@scure/base": "^2.0.0",
|
"@scure/base": "^2.0.0",
|
||||||
"kysely": "^0.28.9",
|
"kysely": "^0.28.9",
|
||||||
"pg": "^8.13.1"
|
"pg": "^8.13.1"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import type { Database } from "@reviq/db-schema";
|
import type { Database } from "@reviq/db-schema";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { hashPassword } from "@reviq/utils";
|
import { hashPassword } from "@reviq/server-utils";
|
||||||
import { generateToken, hashToken } from "./token.js";
|
import { generateToken, hashToken } from "./token.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,5 @@
|
|||||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node", "bun"]
|
"types": ["node", "bun"]
|
||||||
},
|
}
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
packages/frontend-utils/README.md
Normal file
15
packages/frontend-utils/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# @reviq/frontend-utils
|
||||||
|
|
||||||
|
Frontend-specific utilities for the RevIQ publisher dashboard.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getOrgColor, getOrgInitials } from "@reviq/frontend-utils";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
- `getOrgInitials(org)` - Get display initials from an organization's slug or display name
|
||||||
|
- `getOrgColor(org)` - Get a consistent HSL color based on the organization slug
|
||||||
|
- `OrgLike` - Type interface for organization objects
|
||||||
24
packages/server-utils/README.md
Normal file
24
packages/server-utils/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# @reviq/server-utils
|
||||||
|
|
||||||
|
Server and CLI utilities for the RevIQ platform. These utilities use crypto APIs and are designed for Cloudflare Workers compatibility.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
generateSecureBase58Token,
|
||||||
|
hashPassword,
|
||||||
|
verifyPassword,
|
||||||
|
} from "@reviq/server-utils";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
### Token Generation
|
||||||
|
- `generateSecureBase58Token(prefix)` - Generate a secure random token with a prefix (e.g., `reviq_`)
|
||||||
|
- `parseBase58Token(token)` - Parse and validate a base58-encoded token
|
||||||
|
- `isBase58(str)` - Check if a string is valid base58
|
||||||
|
|
||||||
|
### Password Hashing
|
||||||
|
- `hashPassword(password)` - Hash a password using PBKDF2-SHA256
|
||||||
|
- `verifyPassword(password, hash)` - Verify a password against a stored hash
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@reviq/utils",
|
"name": "@reviq/server-utils",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -63,7 +63,7 @@ export const hashPassword = async (password: string): Promise<string> => {
|
|||||||
const saltB64 = toBase64(salt.buffer);
|
const saltB64 = toBase64(salt.buffer);
|
||||||
const hashB64 = toBase64(derivedBits);
|
const hashB64 = toBase64(derivedBits);
|
||||||
|
|
||||||
return `$${ALGORITHM}$${String(ITERATIONS)}$${saltB64}$${hashB64}`;
|
return `$${ALGORITHM}$${ITERATIONS.toString()}$${saltB64}$${hashB64}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyPassword = async (
|
export const verifyPassword = async (
|
||||||
52
packages/testing/test-helpers/README.md
Normal file
52
packages/testing/test-helpers/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# @reviq/test-helpers
|
||||||
|
|
||||||
|
Database testing utilities for integration and e2e tests.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
describeE2E,
|
||||||
|
createTestDb,
|
||||||
|
withTestTransaction,
|
||||||
|
createTestUser,
|
||||||
|
} from "@reviq/test-helpers";
|
||||||
|
|
||||||
|
describeE2E("My API tests", () => {
|
||||||
|
it("should create a user", async () => {
|
||||||
|
await withTestTransaction(async (trx) => {
|
||||||
|
const user = await createTestUser(trx, {
|
||||||
|
email: "test@example.com",
|
||||||
|
});
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
### Test Setup
|
||||||
|
- `describeE2E(name, fn)` - Wrapper for describe() that skips when `SKIP_DB_TESTS=1`
|
||||||
|
- `SKIP_DB_TESTS` - Boolean indicating if db tests should be skipped
|
||||||
|
|
||||||
|
### Database Utilities
|
||||||
|
- `createTestDb()` - Create an isolated test database
|
||||||
|
- `destroyTestDb(db)` - Destroy a test database
|
||||||
|
- `getSharedDb()` - Get the shared test database instance
|
||||||
|
- `destroySharedDb()` - Destroy the shared database
|
||||||
|
- `initTestDb()` - Initialize the test database
|
||||||
|
- `runMigrations(url)` - Run database migrations
|
||||||
|
- `truncateAllTables(db)` - Clear all data from tables
|
||||||
|
- `getTestDatabaseUrl()` - Get the test database connection URL
|
||||||
|
|
||||||
|
### Transaction Helpers
|
||||||
|
- `withTestTransaction(fn)` - Run a function in a rolled-back transaction
|
||||||
|
|
||||||
|
### Fixtures
|
||||||
|
- `createTestUser(trx, opts)` - Create a test user
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
- `TEST_RP` - Test relying party configuration for WebAuthn
|
||||||
|
- `DEFAULT_TEST_AAGUID` - Default AAGUID for virtual authenticator
|
||||||
|
- `KNOWN_AAGUIDS` - Map of known authenticator AAGUIDs
|
||||||
@@ -16,3 +16,4 @@ export {
|
|||||||
truncateAllTables,
|
truncateAllTables,
|
||||||
} from "./test-db.js";
|
} from "./test-db.js";
|
||||||
export { withTestTransaction } from "./test-transaction.js";
|
export { withTestTransaction } from "./test-transaction.js";
|
||||||
|
export { uniqueTestId } from "./test-utils.js";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|||||||
import { createDb } from "@reviq/db";
|
import { createDb } from "@reviq/db";
|
||||||
import { sql } from "kysely";
|
import { sql } from "kysely";
|
||||||
import pg from "pg";
|
import pg from "pg";
|
||||||
|
import { uniqueTestId } from "./test-utils.js";
|
||||||
|
|
||||||
const { Client } = pg;
|
const { Client } = pg;
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ export async function runMigrations(): Promise<void> {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
const stderr = await new Response(proc.stderr).text();
|
const stderr = await new Response(proc.stderr).text();
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Migration failed with code ${String(exitCode)}: ${stderr}`,
|
`Migration failed with code ${exitCode.toString()}: ${stderr}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,7 +225,7 @@ export async function createTestUser(
|
|||||||
isSuperuser: boolean;
|
isSuperuser: boolean;
|
||||||
}> = {},
|
}> = {},
|
||||||
): Promise<{ id: number; email: string }> {
|
): Promise<{ id: number; email: string }> {
|
||||||
const email = overrides.email ?? `test-${String(Date.now())}@example.com`;
|
const email = overrides.email ?? `test-${uniqueTestId()}@example.com`;
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.insertInto("users")
|
.insertInto("users")
|
||||||
|
|||||||
15
packages/testing/test-helpers/src/test-utils.ts
Normal file
15
packages/testing/test-helpers/src/test-utils.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Test utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique test ID using timestamp and random string.
|
||||||
|
* Useful for creating unique emails, slugs, tokens, etc. in tests.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const email = `user-${uniqueTestId()}@example.com`
|
||||||
|
* const slug = `org-${uniqueTestId()}`
|
||||||
|
*/
|
||||||
|
export function uniqueTestId(): string {
|
||||||
|
return `${Date.now().toString()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
39
packages/testing/virtual-authenticator/README.md
Normal file
39
packages/testing/virtual-authenticator/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# @reviq/virtual-authenticator
|
||||||
|
|
||||||
|
WebAuthn virtual authenticator for testing passkey registration and authentication flows.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { VirtualAuthenticator } from "@reviq/virtual-authenticator";
|
||||||
|
|
||||||
|
const authenticator = new VirtualAuthenticator({
|
||||||
|
aaguid: "00000000-0000-0000-0000-000000000000",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a credential during registration
|
||||||
|
const credential = await authenticator.create(
|
||||||
|
publicKeyCredentialCreationOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the credential during authentication
|
||||||
|
const assertion = await authenticator.get(
|
||||||
|
publicKeyCredentialRequestOptions,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
### Classes
|
||||||
|
- `VirtualAuthenticator` - Simulates a WebAuthn authenticator
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
- `base64urlToUint8Array(str)` - Decode base64url to bytes
|
||||||
|
- `uint8ArrayToBase64url(bytes)` - Encode bytes to base64url
|
||||||
|
- `parseAaguid(str)` - Parse an AAGUID string to bytes
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
- `DEFAULT_AAGUID` - Default AAGUID for the virtual authenticator
|
||||||
|
|
||||||
|
### Types
|
||||||
|
- `VirtualAuthenticatorOptions` - Configuration options for the authenticator
|
||||||
@@ -2,6 +2,5 @@
|
|||||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node", "bun"]
|
"types": ["node", "bun"]
|
||||||
},
|
}
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user