11import { Result } from "@adviser/cement" ;
22import { SuperThis } from "@fireproof/core" ;
3- import { gte , and , eq , gt , inArray , lt , ne , or } from "drizzle-orm/sql/expressions" ;
3+ import { and , eq , gt , gte , inArray , lt , ne , or } from "drizzle-orm/sql/expressions" ;
44// import type { LibSQLDatabase } from "drizzle-orm/libsql";
5- import { jwtVerify } from "jose" ;
65import {
76 AuthType ,
87 ClerkClaim ,
98 ClerkVerifyAuth ,
9+ FAPIMsgImpl ,
1010 InCreateTenantParams ,
1111 InviteTicket ,
1212 InvitedParams ,
@@ -26,6 +26,7 @@ import {
2626 ReqListLedgersByUser ,
2727 ReqListTenantsByUser ,
2828 ReqRedeemInvite ,
29+ ReqShareWithUser ,
2930 ReqTokenByResultId ,
3031 ReqUpdateLedger ,
3132 ReqUpdateTenant ,
@@ -44,6 +45,7 @@ import {
4445 ResListLedgersByUser ,
4546 ResListTenantsByUser ,
4647 ResRedeemInvite ,
48+ ResShareWithUser ,
4749 ResTokenByResultId ,
4850 ResUpdateLedger ,
4951 ResUpdateTenant ,
@@ -52,18 +54,18 @@ import {
5254 User ,
5355 UserStatus ,
5456 VerifiedAuth ,
55- FAPIMsgImpl ,
5657} from "@fireproof/core-protocols-dashboard" ;
58+ import { sts } from "@fireproof/core-runtime" ;
59+ import { FPCloudClaim , ReadWrite , Role , toReadWrite , toRole } from "@fireproof/core-types-protocols-cloud" ;
60+ import { jwtVerify } from "jose" ;
61+ import { FPTokenContext , createFPToken , getFPTokenContext } from "./create-fp-token.js" ;
62+ import { DashSqlite } from "./create-handler.js" ;
5763import { prepareInviteTicket , sqlInviteTickets , sqlToInviteTickets } from "./invites.js" ;
5864import { sqlLedgerUsers , sqlLedgers , sqlToLedgers } from "./ledgers.js" ;
5965import { queryCondition , queryEmail , queryNick , toBoolean , toUndef } from "./sql-helper.js" ;
6066import { sqlTenantUsers , sqlTenants } from "./tenants.js" ;
6167import { sqlTokenByResultId } from "./token-by-result-id.js" ;
6268import { UserNotFoundError , getUser , isUserNotFound , queryUser , upsetUserByProvider } from "./users.js" ;
63- import { createFPToken , FPTokenContext , getFPTokenContext } from "./create-fp-token.js" ;
64- import { Role , ReadWrite , toRole , toReadWrite , FPCloudClaim } from "@fireproof/core-types-protocols-cloud" ;
65- import { sts } from "@fireproof/core-runtime" ;
66- import { DashSqlite } from "./create-handler.js" ;
6769
6870function sqlToOutTenantParams ( sql : typeof sqlTenants . $inferSelect ) : OutTenantParams {
6971 return {
@@ -110,6 +112,7 @@ export interface FPApiInterface {
110112 listLedgersByUser ( req : ReqListLedgersByUser ) : Promise < Result < ResListLedgersByUser > > ;
111113 updateLedger ( req : ReqUpdateLedger ) : Promise < Result < ResUpdateLedger > > ;
112114 deleteLedger ( req : ReqDeleteLedger ) : Promise < Result < ResDeleteLedger > > ;
115+ shareWithUser ( req : ReqShareWithUser ) : Promise < Result < ResShareWithUser > > ;
113116
114117 // listLedgersByTenant(req: ReqListLedgerByTenant): Promise<ResListLedgerByTenant>
115118
@@ -1709,6 +1712,109 @@ export class FPApiSQL implements FPApiInterface {
17091712 ledgerId : req . ledger . ledgerId ,
17101713 } ) ;
17111714 }
1715+
1716+ async shareWithUser ( req : ReqShareWithUser , ictx : Partial < FPTokenContext > = { } ) : Promise < Result < ResShareWithUser > > {
1717+ // 1. Get JWT context and verify token
1718+ const rCtx = await getFPTokenContext ( this . sthis , ictx ) ;
1719+ if ( rCtx . isErr ( ) ) {
1720+ return Result . Err ( rCtx . Err ( ) ) ;
1721+ }
1722+ const ctx = rCtx . Ok ( ) ;
1723+
1724+ const rPayload = await this . verifyFPToken ( req . auth . token , ctx ) ;
1725+ if ( rPayload . isErr ( ) ) {
1726+ return Result . Err ( rPayload . Err ( ) ) ;
1727+ }
1728+ const payload = rPayload . Ok ( ) ;
1729+
1730+ // 2. Extract user ID and ledger ID from token
1731+ if ( ! payload . userId ) {
1732+ return Result . Err ( "No user ID in token" ) ;
1733+ }
1734+
1735+ if ( ! payload . selected ?. ledger ) {
1736+ return Result . Err ( "No ledger selected in token" ) ;
1737+ }
1738+
1739+ const userId = payload . userId ;
1740+ const ledgerId = payload . selected . ledger ;
1741+
1742+ // 3. Check if user is admin of the ledger
1743+ if ( ! ( await this . isAdminOfLedger ( userId , ledgerId ) ) ) {
1744+ return Result . Err ( "Not authorized to share this ledger. Admin access required." ) ;
1745+ }
1746+
1747+ // 4. Get ledger details
1748+ const ledger = await this . db
1749+ . select ( )
1750+ . from ( sqlLedgers )
1751+ . where ( and ( eq ( sqlLedgers . ledgerId , ledgerId ) , eq ( sqlLedgers . status , "active" ) ) )
1752+ . get ( ) ;
1753+
1754+ if ( ! ledger ) {
1755+ return Result . Err ( "Ledger not found or inactive" ) ;
1756+ }
1757+
1758+ // 5. Find user by email
1759+ const rUser = await queryUser ( this . db , { byString : req . email } ) ;
1760+ if ( rUser . isErr ( ) ) {
1761+ return Result . Err ( rUser . Err ( ) ) ;
1762+ }
1763+
1764+ const users = rUser . Ok ( ) ;
1765+ if ( users . length === 0 ) {
1766+ return Result . Err ( `User with email ${ req . email } not found. User must sign up first.` ) ;
1767+ }
1768+
1769+ if ( users . length > 1 ) {
1770+ return Result . Err ( `Multiple users found for email ${ req . email } ` ) ;
1771+ }
1772+
1773+ const targetUser = users [ 0 ] ;
1774+
1775+ // Prevent self-sharing
1776+ if ( targetUser . userId === userId ) {
1777+ return Result . Err ( "Cannot share ledger with yourself" ) ;
1778+ }
1779+
1780+ // 6. Add user to tenant first
1781+ const rAddUserToTenant = await this . addUserToTenant ( this . db , {
1782+ userName : req . email ,
1783+ tenantId : ledger . tenantId ,
1784+ userId : targetUser . userId ,
1785+ role : "member" ,
1786+ } ) ;
1787+
1788+ if ( rAddUserToTenant . isErr ( ) ) {
1789+ return Result . Err ( rAddUserToTenant . Err ( ) ) ;
1790+ }
1791+
1792+ // 7. Add user to ledger
1793+ const rAddUser = await this . addUserToLedger ( this . db , {
1794+ userName : req . email ,
1795+ ledgerId : ledgerId ,
1796+ tenantId : ledger . tenantId ,
1797+ userId : targetUser . userId ,
1798+ role : req . role || "member" ,
1799+ right : req . right || "read" ,
1800+ } ) ;
1801+
1802+ if ( rAddUser . isErr ( ) ) {
1803+ return Result . Err ( rAddUser . Err ( ) ) ;
1804+ }
1805+
1806+ return Result . Ok ( {
1807+ type : "resShareWithUser" ,
1808+ success : true ,
1809+ message : `Successfully shared ledger with ${ req . email } ` ,
1810+ ledgerId : ledgerId ,
1811+ userId : targetUser . userId ,
1812+ email : req . email ,
1813+ role : req . role || "member" ,
1814+ right : req . right || "write" ,
1815+ } ) ;
1816+ }
1817+
17121818 async listLedgersByUser ( req : ReqListLedgersByUser ) : Promise < Result < ResListLedgersByUser > > {
17131819 const rAuth = await this . activeUser ( req ) ;
17141820 if ( rAuth . isErr ( ) ) {
@@ -1899,21 +2005,10 @@ export class FPApiSQL implements FPApiInterface {
18992005 } ) ;
19002006 }
19012007
1902- /**
1903- * Extract token from request, validate it, and extend expiry by 1 day
1904- */
1905- async extendToken ( req : ReqExtendToken , ictx : Partial < FPTokenContext > = { } ) : Promise < Result < ResExtendToken > > {
1906- const rCtx = await getFPTokenContext ( this . sthis , ictx ) ;
1907- if ( rCtx . isErr ( ) ) {
1908- return Result . Err ( rCtx . Err ( ) ) ;
1909- }
1910- const ctx = rCtx . Ok ( ) ;
2008+ private async verifyFPToken ( token : string , ctx : FPTokenContext ) : Promise < Result < FPCloudClaim > > {
19112009 try {
1912- // Get the public key for verification
19132010 const pubKey = await sts . env2jwk ( ctx . publicToken , "ES256" ) ;
1914-
1915- // Verify the token
1916- const verifyResult = await jwtVerify ( req . token , pubKey , {
2011+ const verifyResult = await jwtVerify ( token , pubKey , {
19172012 issuer : ctx . issuer ,
19182013 audience : ctx . audience ,
19192014 } ) ;
@@ -1924,22 +2019,41 @@ export class FPApiSQL implements FPApiInterface {
19242019 if ( ! payload . exp || payload . exp * 1000 <= now ) {
19252020 return Result . Err ( "Token is expired" ) ;
19262021 }
1927- // Create new token with extended expiry using the private key
1928- // JWT expects expiration time in seconds, not milliseconds
1929- const newToken = await createFPToken (
1930- {
1931- ...ctx ,
1932- validFor : ctx . extendValidFor ,
1933- } ,
1934- payload ,
1935- ) ;
1936- return Result . Ok ( {
1937- type : "resExtendToken" ,
1938- token : newToken ,
1939- } ) ;
2022+
2023+ return Result . Ok ( payload ) ;
19402024 } catch ( error ) {
1941- return Result . Err ( `Token validation failed: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
2025+ return Result . Err ( `Token verification failed: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
2026+ }
2027+ }
2028+
2029+ /**
2030+ * Extract token from request, validate it, and extend expiry by 1 day
2031+ */
2032+ async extendToken ( req : ReqExtendToken , ictx : Partial < FPTokenContext > = { } ) : Promise < Result < ResExtendToken > > {
2033+ const rCtx = await getFPTokenContext ( this . sthis , ictx ) ;
2034+ if ( rCtx . isErr ( ) ) {
2035+ return Result . Err ( rCtx . Err ( ) ) ;
2036+ }
2037+ const ctx = rCtx . Ok ( ) ;
2038+
2039+ const rPayload = await this . verifyFPToken ( req . token , ctx ) ;
2040+ if ( rPayload . isErr ( ) ) {
2041+ return Result . Err ( rPayload . Err ( ) ) ;
19422042 }
2043+
2044+ // Create new token with extended expiry
2045+ const newToken = await createFPToken (
2046+ {
2047+ ...ctx ,
2048+ validFor : ctx . extendValidFor ,
2049+ } ,
2050+ rPayload . Ok ( ) ,
2051+ ) ;
2052+
2053+ return Result . Ok ( {
2054+ type : "resExtendToken" ,
2055+ token : newToken ,
2056+ } ) ;
19432057 }
19442058}
19452059
0 commit comments