347 lines
8.3 KiB
Swift
347 lines
8.3 KiB
Swift
|
|
import Foundation
|
||
|
|
|
||
|
|
enum AppSection: String, CaseIterable, Identifiable, Hashable {
|
||
|
|
case overview
|
||
|
|
case requests
|
||
|
|
case activity
|
||
|
|
case account
|
||
|
|
|
||
|
|
var id: String { rawValue }
|
||
|
|
|
||
|
|
var title: String {
|
||
|
|
switch self {
|
||
|
|
case .overview: "Passport"
|
||
|
|
case .requests: "Requests"
|
||
|
|
case .activity: "Activity"
|
||
|
|
case .account: "Account"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var systemImage: String {
|
||
|
|
switch self {
|
||
|
|
case .overview: "person.crop.square.fill"
|
||
|
|
case .requests: "checklist.checked"
|
||
|
|
case .activity: "clock.arrow.trianglehead.counterclockwise.rotate.90"
|
||
|
|
case .account: "person.crop.circle.fill"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum NotificationPermissionState: String, CaseIterable, Identifiable {
|
||
|
|
case unknown
|
||
|
|
case allowed
|
||
|
|
case provisional
|
||
|
|
case denied
|
||
|
|
|
||
|
|
var id: String { rawValue }
|
||
|
|
|
||
|
|
var title: String {
|
||
|
|
switch self {
|
||
|
|
case .unknown: "Not Asked Yet"
|
||
|
|
case .allowed: "Enabled"
|
||
|
|
case .provisional: "Delivered Quietly"
|
||
|
|
case .denied: "Disabled"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var systemImage: String {
|
||
|
|
switch self {
|
||
|
|
case .unknown: "bell"
|
||
|
|
case .allowed: "bell.badge.fill"
|
||
|
|
case .provisional: "bell.badge"
|
||
|
|
case .denied: "bell.slash.fill"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var summary: String {
|
||
|
|
switch self {
|
||
|
|
case .unknown:
|
||
|
|
"The app has not asked for notification delivery yet."
|
||
|
|
case .allowed:
|
||
|
|
"Alerts can break through immediately when a request arrives."
|
||
|
|
case .provisional:
|
||
|
|
"Notifications can be delivered quietly until the user promotes them."
|
||
|
|
case .denied:
|
||
|
|
"Approval events stay in-app until the user re-enables notifications."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct BootstrapContext {
|
||
|
|
let suggestedQRCodePayload: String
|
||
|
|
}
|
||
|
|
|
||
|
|
struct DashboardSnapshot {
|
||
|
|
let profile: MemberProfile
|
||
|
|
let requests: [ApprovalRequest]
|
||
|
|
let notifications: [AppNotification]
|
||
|
|
}
|
||
|
|
|
||
|
|
struct SignInResult {
|
||
|
|
let session: AuthSession
|
||
|
|
let snapshot: DashboardSnapshot
|
||
|
|
}
|
||
|
|
|
||
|
|
struct MemberProfile: Identifiable, Hashable {
|
||
|
|
let id: UUID
|
||
|
|
let name: String
|
||
|
|
let handle: String
|
||
|
|
let organization: String
|
||
|
|
let deviceCount: Int
|
||
|
|
let recoverySummary: String
|
||
|
|
|
||
|
|
init(
|
||
|
|
id: UUID = UUID(),
|
||
|
|
name: String,
|
||
|
|
handle: String,
|
||
|
|
organization: String,
|
||
|
|
deviceCount: Int,
|
||
|
|
recoverySummary: String
|
||
|
|
) {
|
||
|
|
self.id = id
|
||
|
|
self.name = name
|
||
|
|
self.handle = handle
|
||
|
|
self.organization = organization
|
||
|
|
self.deviceCount = deviceCount
|
||
|
|
self.recoverySummary = recoverySummary
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct AuthSession: Identifiable, Hashable {
|
||
|
|
let id: UUID
|
||
|
|
let deviceName: String
|
||
|
|
let originHost: String
|
||
|
|
let pairedAt: Date
|
||
|
|
let tokenPreview: String
|
||
|
|
let pairingCode: String
|
||
|
|
|
||
|
|
init(
|
||
|
|
id: UUID = UUID(),
|
||
|
|
deviceName: String,
|
||
|
|
originHost: String,
|
||
|
|
pairedAt: Date,
|
||
|
|
tokenPreview: String,
|
||
|
|
pairingCode: String
|
||
|
|
) {
|
||
|
|
self.id = id
|
||
|
|
self.deviceName = deviceName
|
||
|
|
self.originHost = originHost
|
||
|
|
self.pairedAt = pairedAt
|
||
|
|
self.tokenPreview = tokenPreview
|
||
|
|
self.pairingCode = pairingCode
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum ApprovalRequestKind: String, CaseIterable, Hashable {
|
||
|
|
case signIn
|
||
|
|
case accessGrant
|
||
|
|
case elevatedAction
|
||
|
|
|
||
|
|
var title: String {
|
||
|
|
switch self {
|
||
|
|
case .signIn: "Sign-In"
|
||
|
|
case .accessGrant: "Access Grant"
|
||
|
|
case .elevatedAction: "Elevated Action"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var systemImage: String {
|
||
|
|
switch self {
|
||
|
|
case .signIn: "qrcode.viewfinder"
|
||
|
|
case .accessGrant: "key.fill"
|
||
|
|
case .elevatedAction: "shield.lefthalf.filled"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum ApprovalRisk: String, Hashable {
|
||
|
|
case routine
|
||
|
|
case elevated
|
||
|
|
|
||
|
|
var title: String {
|
||
|
|
switch self {
|
||
|
|
case .routine: "Routine"
|
||
|
|
case .elevated: "Elevated"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var summary: String {
|
||
|
|
switch self {
|
||
|
|
case .routine:
|
||
|
|
"Routine access to profile or sign-in scopes."
|
||
|
|
case .elevated:
|
||
|
|
"Sensitive access that can sign, publish, or unlock privileged actions."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var guidance: String {
|
||
|
|
switch self {
|
||
|
|
case .routine:
|
||
|
|
"Review the origin and scope list, then approve if the session matches the device you expect."
|
||
|
|
case .elevated:
|
||
|
|
"Treat this like a privileged operation. Verify the origin, the requested scopes, and whether the action is time-bound before approving."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum ApprovalStatus: String, Hashable {
|
||
|
|
case pending
|
||
|
|
case approved
|
||
|
|
case rejected
|
||
|
|
|
||
|
|
var title: String {
|
||
|
|
switch self {
|
||
|
|
case .pending: "Pending"
|
||
|
|
case .approved: "Approved"
|
||
|
|
case .rejected: "Rejected"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var systemImage: String {
|
||
|
|
switch self {
|
||
|
|
case .pending: "clock.badge"
|
||
|
|
case .approved: "checkmark.circle.fill"
|
||
|
|
case .rejected: "xmark.circle.fill"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct ApprovalRequest: Identifiable, Hashable {
|
||
|
|
let id: UUID
|
||
|
|
let title: String
|
||
|
|
let subtitle: String
|
||
|
|
let source: String
|
||
|
|
let createdAt: Date
|
||
|
|
let kind: ApprovalRequestKind
|
||
|
|
let risk: ApprovalRisk
|
||
|
|
let scopes: [String]
|
||
|
|
var status: ApprovalStatus
|
||
|
|
|
||
|
|
init(
|
||
|
|
id: UUID = UUID(),
|
||
|
|
title: String,
|
||
|
|
subtitle: String,
|
||
|
|
source: String,
|
||
|
|
createdAt: Date,
|
||
|
|
kind: ApprovalRequestKind,
|
||
|
|
risk: ApprovalRisk,
|
||
|
|
scopes: [String],
|
||
|
|
status: ApprovalStatus
|
||
|
|
) {
|
||
|
|
self.id = id
|
||
|
|
self.title = title
|
||
|
|
self.subtitle = subtitle
|
||
|
|
self.source = source
|
||
|
|
self.createdAt = createdAt
|
||
|
|
self.kind = kind
|
||
|
|
self.risk = risk
|
||
|
|
self.scopes = scopes
|
||
|
|
self.status = status
|
||
|
|
}
|
||
|
|
|
||
|
|
var scopeSummary: String {
|
||
|
|
if scopes.isEmpty {
|
||
|
|
return "No scopes listed"
|
||
|
|
}
|
||
|
|
|
||
|
|
let suffix = scopes.count == 1 ? "" : "s"
|
||
|
|
return "\(scopes.count) requested scope\(suffix)"
|
||
|
|
}
|
||
|
|
|
||
|
|
var trustHeadline: String {
|
||
|
|
switch (kind, risk) {
|
||
|
|
case (.signIn, .routine):
|
||
|
|
"Low-friction sign-in request"
|
||
|
|
case (.signIn, .elevated):
|
||
|
|
"Privileged sign-in request"
|
||
|
|
case (.accessGrant, _):
|
||
|
|
"Token grant request"
|
||
|
|
case (.elevatedAction, _):
|
||
|
|
"Sensitive action request"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var trustDetail: String {
|
||
|
|
switch kind {
|
||
|
|
case .signIn:
|
||
|
|
"This request usually creates or refreshes a session token for a browser, CLI, or device."
|
||
|
|
case .accessGrant:
|
||
|
|
"This request issues scoped access for a service or automation that wants to act on your behalf."
|
||
|
|
case .elevatedAction:
|
||
|
|
"This request performs a privileged action such as signing, publishing, or creating short-lived credentials."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum AppNotificationKind: String, Hashable {
|
||
|
|
case approval
|
||
|
|
case security
|
||
|
|
case system
|
||
|
|
|
||
|
|
var title: String {
|
||
|
|
switch self {
|
||
|
|
case .approval: "Approval"
|
||
|
|
case .security: "Security"
|
||
|
|
case .system: "System"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var systemImage: String {
|
||
|
|
switch self {
|
||
|
|
case .approval: "checkmark.seal.fill"
|
||
|
|
case .security: "shield.fill"
|
||
|
|
case .system: "sparkles"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var summary: String {
|
||
|
|
switch self {
|
||
|
|
case .approval:
|
||
|
|
"Decision and approval activity"
|
||
|
|
case .security:
|
||
|
|
"Pairing and security posture updates"
|
||
|
|
case .system:
|
||
|
|
"Product and environment status messages"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct AppNotification: Identifiable, Hashable {
|
||
|
|
let id: UUID
|
||
|
|
let title: String
|
||
|
|
let message: String
|
||
|
|
let sentAt: Date
|
||
|
|
let kind: AppNotificationKind
|
||
|
|
var isUnread: Bool
|
||
|
|
|
||
|
|
init(
|
||
|
|
id: UUID = UUID(),
|
||
|
|
title: String,
|
||
|
|
message: String,
|
||
|
|
sentAt: Date,
|
||
|
|
kind: AppNotificationKind,
|
||
|
|
isUnread: Bool
|
||
|
|
) {
|
||
|
|
self.id = id
|
||
|
|
self.title = title
|
||
|
|
self.message = message
|
||
|
|
self.sentAt = sentAt
|
||
|
|
self.kind = kind
|
||
|
|
self.isUnread = isUnread
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum AppError: LocalizedError {
|
||
|
|
case invalidQRCode
|
||
|
|
case requestNotFound
|
||
|
|
|
||
|
|
var errorDescription: String? {
|
||
|
|
switch self {
|
||
|
|
case .invalidQRCode:
|
||
|
|
"That QR payload is not valid for idp.global sign-in."
|
||
|
|
case .requestNotFound:
|
||
|
|
"The selected request could not be found."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|