diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01a50fa93a..8417800e98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
* Adds basic sigchain functions ([#2625](https://github.com/TryQuiet/quiet/issues/2625))
* Instantiates signature chain when creating communities and reloading application ([#2626](https://github.com/TryQuiet/quiet/issues/2626))
* Added in LFA-ready invite links ([#2627](https://github.com/TryQuiet/quiet/issues/2627))
+* Generating LFA-ready invite links when a sigchain is configured ([#2627](https://github.com/TryQuiet/quiet/issues/2627))
### Fixes
* Changed company name in app to "A Quiet LLC" ([#2642] (https://github.com/TryQuiet/quiet/issues/2642))
diff --git a/packages/backend/src/nest/auth/services/invites/invite.service.ts b/packages/backend/src/nest/auth/services/invites/invite.service.ts
index dbe220e893..2ac8e9547a 100644
--- a/packages/backend/src/nest/auth/services/invites/invite.service.ts
+++ b/packages/backend/src/nest/auth/services/invites/invite.service.ts
@@ -21,6 +21,8 @@ const logger = createLogger('auth:inviteService')
export const DEFAULT_MAX_USES = 1
export const DEFAULT_INVITATION_VALID_FOR_MS = 604_800_000 // 1 week
+export const DEFAULT_LONG_LIVED_MAX_USES = 0 // no limit
+export const DEFAULT_LONG_LIVED_VALID_FOR_MS = 0 // no limit
class InviteService extends ChainServiceBase {
public static init(sigChain: SigChain): InviteService {
@@ -32,7 +34,11 @@ class InviteService extends ChainServiceBase {
maxUses: number = DEFAULT_MAX_USES,
seed?: string
): InviteResult {
- const expiration = (Date.now() + validForMs) as UnixTimestamp
+ let expiration: UnixTimestamp = 0 as UnixTimestamp
+ if (validForMs > 0) {
+ expiration = (Date.now() + validForMs) as UnixTimestamp
+ }
+
const invitation: InviteResult = this.sigChain.team.inviteMember({
seed,
expiration,
@@ -41,6 +47,10 @@ class InviteService extends ChainServiceBase {
return invitation
}
+ public createLongLivedUserInvite(): InviteResult {
+ return this.createUserInvite(DEFAULT_LONG_LIVED_VALID_FOR_MS, DEFAULT_LONG_LIVED_MAX_USES)
+ }
+
public createDeviceInvite(validForMs: number = DEFAULT_INVITATION_VALID_FOR_MS, seed?: string): InviteResult {
const expiration = (Date.now() + validForMs) as UnixTimestamp
const invitation: InviteResult = this.sigChain.team.inviteDevice({
@@ -50,6 +60,23 @@ class InviteService extends ChainServiceBase {
return invitation
}
+ public isValidLongLivedUserInvite(id: Base58): boolean {
+ logger.info(`Validating LFA invite with ID ${id}`)
+ const invites = this.getAllInvites()
+ for (const invite of invites) {
+ if (
+ invite.id === id && // is correct invite
+ !invite.revoked && // is not revoked
+ invite.maxUses == 0 && // is an unlimited invite
+ invite.expiration == 0 // is an unlimited invite
+ ) {
+ return true
+ }
+ }
+
+ return false
+ }
+
public revoke(id: string) {
this.sigChain.team.revokeInvitation(id)
}
diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts
index a1bdbd16a4..47de201ffc 100644
--- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts
+++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts
@@ -12,7 +12,7 @@ import { getLibp2pAddressesFromCsrs, removeFilesFromDir } from '../common/utils'
import { LazyModuleLoader } from '@nestjs/core'
import { createLibp2pAddress, filterValidAddresses, isPSKcodeValid } from '@quiet/common'
-import { CertFieldsTypes, createRootCA, getCertFieldValue, loadCertificate } from '@quiet/identity'
+import { CertFieldsTypes, getCertFieldValue, loadCertificate } from '@quiet/identity'
import {
ChannelMessageIdsResponse,
ChannelSubscribedPayload,
@@ -28,8 +28,6 @@ import {
FileMetadata,
GetMessagesPayload,
InitCommunityPayload,
- InvitationDataV2,
- InvitationDataVersion,
MessagesLoadedPayload,
NetworkDataPayload,
NetworkInfo,
@@ -62,7 +60,6 @@ import { SocketService } from '../socket/socket.service'
import { StorageService } from '../storage/storage.service'
import { StorageEvents } from '../storage/storage.types'
import { StorageServiceClient } from '../storageServiceClient/storageServiceClient.service'
-import { ServerStoredCommunityMetadata } from '../storageServiceClient/storageServiceClient.types'
import { Tor } from '../tor/tor.service'
import { ConfigOptions, GetPorts, ServerIoProviderTypes } from '../types'
import { ServiceState, TorInitState } from './connections-manager.types'
@@ -71,6 +68,7 @@ import { createLogger } from '../common/logger'
import { createUserCsr, getPubKey, loadPrivateKey, pubKeyFromCsr } from '@quiet/identity'
import { config } from '@quiet/state-manager'
import { SigChainService } from '../auth/sigchain.service'
+import { Base58, InviteResult } from '3rd-party/auth/packages/auth/dist'
@Injectable()
export class ConnectionsManagerService extends EventEmitter implements OnModuleInit {
@@ -589,7 +587,12 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
this.logger.error('Community name is required to create sigchain')
return community
}
- this.sigChainService.createChain(community.name, identity.nickname, true)
+
+ this.logger.info(`Creating new LFA chain`)
+ await this.sigChainService.createChain(community.name, identity.nickname, true)
+ // this is the forever invite that all users get
+ this.logger.info(`Creating long lived LFA invite code`)
+ this.socketService.emit(SocketActionTypes.CREATE_LONG_LIVED_LFA_INVITE)
return community
}
@@ -908,13 +911,56 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
callback(await this.leaveCommunity())
})
+ // Local First Auth
+
+ this.socketService.on(
+ SocketActionTypes.CREATE_LONG_LIVED_LFA_INVITE,
+ async (callback?: (response: InviteResult | undefined) => void) => {
+ this.logger.info(`socketService - ${SocketActionTypes.CREATE_LONG_LIVED_LFA_INVITE}`)
+ if (this.sigChainService.activeChainTeamName != null) {
+ const invite = this.sigChainService.getActiveChain().invites.createLongLivedUserInvite()
+ this.serverIoProvider.io.emit(SocketActionTypes.CREATED_LONG_LIVED_LFA_INVITE, invite)
+ if (callback) callback(invite)
+ } else {
+ this.logger.warn(`No sigchain configured, skipping long lived LFA invite code generation!`)
+ if (callback) callback(undefined)
+ }
+ }
+ )
+
+ this.socketService.on(
+ SocketActionTypes.VALIDATE_OR_CREATE_LONG_LIVED_LFA_INVITE,
+ async (
+ inviteId: Base58,
+ callback: (response: { isValid: boolean; newInvite?: InviteResult } | undefined) => void
+ ) => {
+ this.logger.info(`socketService - ${SocketActionTypes.VALIDATE_OR_CREATE_LONG_LIVED_LFA_INVITE}`)
+ if (this.sigChainService.activeChainTeamName != null) {
+ if (this.sigChainService.getActiveChain().invites.isValidLongLivedUserInvite(inviteId)) {
+ this.logger.info(`Invite is a valid long lived LFA invite code!`)
+ callback({ isValid: true })
+ } else {
+ this.logger.info(`Invite is an invalid long lived LFA invite code! Generating a new code!`)
+ const newInvite = this.sigChainService.getActiveChain().invites.createLongLivedUserInvite()
+ this.serverIoProvider.io.emit(SocketActionTypes.CREATED_LONG_LIVED_LFA_INVITE, newInvite)
+ callback({ isValid: false, newInvite })
+ }
+ } else {
+ this.logger.warn(`No sigchain configured, skipping long lived LFA invite code validation/generation!`)
+ callback(undefined)
+ }
+ }
+ )
+
// Username registration
+
this.socketService.on(SocketActionTypes.ADD_CSR, async (payload: SaveCSRPayload) => {
this.logger.info(`socketService - ${SocketActionTypes.ADD_CSR}`)
await this.storageService?.saveCSR(payload)
})
// Public Channels
+
this.socketService.on(
SocketActionTypes.CREATE_CHANNEL,
async (args: CreateChannelPayload, callback: (response?: CreateChannelResponse) => void) => {
diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts
index 41e95ea66d..bfa31bc5b3 100644
--- a/packages/backend/src/nest/socket/socket.service.ts
+++ b/packages/backend/src/nest/socket/socket.service.ts
@@ -25,6 +25,7 @@ import { ConfigOptions, ServerIoProviderTypes } from '../types'
import { suspendableSocketEvents } from './suspendable.events'
import { createLogger } from '../common/logger'
import type net from 'node:net'
+import { Base58, InviteResult } from '@localfirst/auth'
@Injectable()
export class SocketService extends EventEmitter implements OnModuleInit {
@@ -205,6 +206,29 @@ export class SocketService extends EventEmitter implements OnModuleInit {
this.emit(SocketActionTypes.SET_USER_PROFILE, profile)
})
+ // ====== Local First Auth ======
+
+ socket.on(
+ SocketActionTypes.CREATE_LONG_LIVED_LFA_INVITE,
+ async (callback: (response: InviteResult | undefined) => void) => {
+ this.logger.info(`Creating long lived LFA invite code`)
+ this.emit(SocketActionTypes.CREATE_LONG_LIVED_LFA_INVITE, callback)
+ }
+ )
+
+ socket.on(
+ SocketActionTypes.VALIDATE_OR_CREATE_LONG_LIVED_LFA_INVITE,
+ async (inviteId: Base58, callback: (response: InviteResult | undefined) => void) => {
+ this.logger.info(`Validating long lived LFA invite with ID ${inviteId} or creating a new one`)
+ this.emit(SocketActionTypes.VALIDATE_OR_CREATE_LONG_LIVED_LFA_INVITE, inviteId, callback)
+ }
+ )
+
+ socket.on(SocketActionTypes.CREATED_LONG_LIVED_LFA_INVITE, (invite: InviteResult) => {
+ this.logger.info(`Created new long lived LFA invite code with id ${invite.id}`)
+ this.emit(SocketActionTypes.CREATED_LONG_LIVED_LFA_INVITE, invite)
+ })
+
// ====== Misc ======
socket.on(SocketActionTypes.LOAD_MIGRATION_DATA, async (data: Record
+ Anyone with Quiet app can follow this link to join this community.
+
+ Only share with people you trust.
+
+ •••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• +
+ +
+ Anyone with Quiet app can follow this link to join this community.
+
+ Only share with people you trust.
+
+ https://tryquiet.org/join#p=QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3%2Cp3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad&k=12345&o=testOwnerOrbitDbIdentity +
+ +