feat: complete Tasks 21-28 — backend integration, security hardening, UI tests & CI

- Add Apple Sign-In backend (JWKS verification, account linking, session management)
- Implement push notification deep linking with NotificationDeepLinkRouter
- Add jailbreak detection, runtime integrity monitoring, secure enclave service
- Implement OAuth social login, token refresh, and secure logout flows
- Add image caching (memory/disk), optimizer, upload queue, async semaphore
- Implement notification analytics, type preferences, and category setup
- Expand UI test suite with UITestBase, accessibility, auth flow, performance tests
- Add CI pipeline for iOS UI tests (3 device sizes) and performance benchmarks
- Restructure Xcode project to manual groups with KordantWidgets target
- Add SwiftLint, Swift Collections/Algorithms/GoogleSignIn dependencies
- Update project.yml for XcodeGen with new targets and configurations
This commit is contained in:
2026-06-02 15:01:38 -04:00
parent ab0d4857db
commit e33ddf3002
49 changed files with 10472 additions and 421 deletions

View File

@@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server";
import { eq, and, isNull } from "drizzle-orm";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { db } from "~/server/db";
import { users, accounts } from "~/server/db/schema/auth";
import { hashPassword, verifyPassword } from "~/server/auth/password";
@@ -67,6 +68,8 @@ export async function authenticateUser(
}
const GOOGLE_ISSUER = "https://accounts.google.com";
const APPLE_ISSUER = "https://appleid.apple.com";
const APPLE_JWKS_URL = new URL("https://appleid.apple.com/auth/keys");
/**
* Verifies a Google ID token using firebase-admin and returns the user.
@@ -207,6 +210,137 @@ export async function authenticateWithGoogle(idToken: string) {
return { user, sessionToken: session.sessionToken, accessToken, refreshToken, isNewUser };
}
/**
* Verifies an Apple identity token and authenticates the user.
* If the user does not exist, creates a new account.
* If the user exists but has not linked Apple, links the provider.
*/
export async function authenticateWithApple(
identityToken: string,
authorizationCode: string,
userIdentifier?: string | null,
) {
if (!identityToken) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Missing identity token",
});
}
// Verify Apple ID token using Apple's JWKS
let payload: { sub: string; email?: string; is_private_email?: string; };
try {
const JWKS = createRemoteJWKSet(APPLE_JWKS_URL);
const result = await jwtVerify(identityToken, JWKS, {
issuer: APPLE_ISSUER,
audience: process.env.IOS_BUNDLE_ID ?? "com.frenocorp.kordant",
});
payload = result.payload as unknown as { sub: string; email?: string; is_private_email?: string; };
} catch (err) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid Apple identity token",
});
}
const appleUserId = payload.sub;
const email = payload.email ?? null;
if (!email) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Apple account has no email address",
});
}
// Check if this Apple account is already linked
const [existingAccount] = await db
.select()
.from(accounts)
.where(
and(
eq(accounts.provider, "apple"),
eq(accounts.providerAccountId, appleUserId),
),
)
.limit(1);
let userId: string;
let isNewUser = false;
if (existingAccount) {
// Already linked — use the existing user
userId = existingAccount.userId;
isNewUser = false;
// Update tokens
await db
.update(accounts)
.set({
accessToken: identityToken,
refreshToken: authorizationCode,
updatedAt: new Date(),
})
.where(eq(accounts.id, existingAccount.id));
} else {
// Not linked — check if a user with this email exists
const [existingUserByEmail] = await db
.select()
.from(users)
.where(and(eq(users.email, email), isNull(users.deletedAt)))
.limit(1);
// Apple provides the user's first name and last name only on the initial sign-up
// We derive a display name from email if userIdentifier-based lookup doesn't work
const displayName = email.split("@")[0] ?? "User";
if (existingUserByEmail) {
// Link Apple to existing user
userId = existingUserByEmail.id;
isNewUser = false;
await db.insert(accounts).values({
userId,
provider: "apple",
providerAccountId: appleUserId,
accessToken: identityToken,
refreshToken: authorizationCode,
});
} else {
// Create new user with Apple
isNewUser = true;
const [newUser] = await db
.insert(users)
.values({
name: displayName,
email,
emailVerified: new Date(),
})
.returning();
userId = newUser.id;
await db.insert(accounts).values({
userId,
provider: "apple",
providerAccountId: appleUserId,
accessToken: identityToken,
refreshToken: authorizationCode,
});
}
}
// Create session and JWT
const session = await createSession(userId);
const accessToken = await signJWT({ sub: userId }, { expiresIn: "7d" });
const refreshToken = await signJWT({ sub: userId, type: "refresh" }, { expiresIn: "30d" });
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
if (!user) {
throw new TRPCError({ code: "NOT_FOUND", message: "User not found after creation" });
}
return { user, sessionToken: session.sessionToken, accessToken, refreshToken, isNewUser };
}
/**
* Refreshes an access token using a valid refresh token.
*/