platforms.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import { defineStore } from 'pinia'
  2. import { ref, computed } from 'vue'
  3. import axios from 'axios'
  4. export interface PlatformStatus {
  5. platform: string
  6. connected: boolean
  7. username?: string
  8. displayName?: string
  9. avatar?: string
  10. error?: string
  11. pageCount?: number
  12. accountCount?: number
  13. }
  14. export interface MetaPage {
  15. id: string
  16. name: string
  17. picture?: string
  18. }
  19. export interface MetaIgAccount {
  20. id: string
  21. username: string
  22. avatar?: string
  23. pageId: string
  24. }
  25. export interface MetaDiscovery {
  26. pages: MetaPage[]
  27. igAccounts: MetaIgAccount[]
  28. }
  29. export interface PinterestBoard {
  30. id: string
  31. name: string
  32. privacy?: string
  33. selected?: boolean
  34. }
  35. export interface PinterestCredentials {
  36. configured: boolean
  37. clientId?: string
  38. clientSecretHint?: string
  39. }
  40. export interface MetaCredentials {
  41. configured: boolean
  42. appId?: string
  43. appSecretHint?: string
  44. }
  45. export interface TokenExpiryAccount {
  46. id: string
  47. username: string
  48. expiresAt: string | null
  49. daysLeft: number | null
  50. isValid: boolean
  51. }
  52. export const PLATFORM_META: Record<string, { label: string; color: string; icon: string }> = {
  53. twitter: { label: 'Twitter/X', color: '#000000', icon: 'fa-brands fa-x-twitter' },
  54. linkedin: { label: 'LinkedIn', color: '#0077B5', icon: 'fa-brands fa-linkedin' },
  55. mastodon: { label: 'Mastodon', color: '#6364FF', icon: 'fa-brands fa-mastodon' },
  56. bluesky: { label: 'Bluesky', color: '#0085FF', icon: 'fa-solid fa-cloud' },
  57. instagram: { label: 'Instagram', color: '#E1306C', icon: 'fa-brands fa-instagram' },
  58. facebook: { label: 'Facebook', color: '#1877F2', icon: 'fa-brands fa-facebook' },
  59. reddit: { label: 'Reddit', color: '#FF4500', icon: 'fa-brands fa-reddit' },
  60. youtube: { label: 'YouTube', color: '#FF0000', icon: 'fa-brands fa-youtube' },
  61. pinterest: { label: 'Pinterest', color: '#E60023', icon: 'fa-brands fa-pinterest' },
  62. }
  63. export const usePlatformsStore = defineStore('platforms', () => {
  64. const statuses = ref<PlatformStatus[]>([])
  65. const loading = ref(false)
  66. // Meta-specific state
  67. const metaCredentials = ref<MetaCredentials>({ configured: false })
  68. const metaDiscovery = ref<MetaDiscovery | null>(null)
  69. const metaLoading = ref(false)
  70. const metaError = ref<string | null>(null)
  71. // Connected pages/accounts (fetched from gateway)
  72. const connectedPages = ref<MetaPage[]>([])
  73. const connectedIgAccounts = ref<MetaIgAccount[]>([])
  74. // Pinterest
  75. const pinterestCredentials = ref<PinterestCredentials>({ configured: false })
  76. const pinterestLoading = ref(false)
  77. const pinterestError = ref<string | null>(null)
  78. const connectedPinterestBoards = ref<PinterestBoard[]>([])
  79. const allPinterestBoards = ref<PinterestBoard[]>([])
  80. // Token expiry
  81. const tokenExpiry = ref<TokenExpiryAccount[]>([])
  82. const tokenExpiryDismissed = ref(false)
  83. const expiringAccounts = computed(() =>
  84. tokenExpiry.value.filter((a: TokenExpiryAccount) => a.daysLeft !== null && a.daysLeft < 7)
  85. )
  86. const hasExpiryWarning = computed(() =>
  87. !tokenExpiryDismissed.value && expiringAccounts.value.length > 0
  88. )
  89. async function fetchTokenExpiry() {
  90. try {
  91. const res = await axios.get('/api/meta/token-expiry')
  92. tokenExpiry.value = res.data.accounts || []
  93. } catch (err) {
  94. console.error('Token expiry check error:', err)
  95. }
  96. }
  97. function dismissTokenWarning() {
  98. tokenExpiryDismissed.value = true
  99. }
  100. async function refreshMetaTokens() {
  101. const res = await axios.post('/api/meta/token-refresh', {})
  102. // Re-fetch expiry so the banner updates immediately
  103. await fetchTokenExpiry()
  104. return res.data
  105. }
  106. async function fetchMetaConnections() {
  107. try {
  108. const res = await fetch('/api/credentials')
  109. const data = await res.json()
  110. connectedPages.value = data.facebook?.pages || []
  111. connectedIgAccounts.value = data.instagram?.accounts || []
  112. connectedPinterestBoards.value = data.pinterest?.boards || []
  113. allPinterestBoards.value = data.pinterest?.allBoards || []
  114. } catch (_) { /* ignore */ }
  115. }
  116. async function fetchPinterestCredentials() {
  117. try {
  118. const res = await axios.get('/api/credentials/pinterest-app')
  119. pinterestCredentials.value = res.data
  120. } catch (err) {
  121. console.error('Pinterest credentials fetch error:', err)
  122. }
  123. }
  124. async function savePinterestApp(clientId: string, clientSecret: string) {
  125. pinterestLoading.value = true
  126. pinterestError.value = null
  127. try {
  128. await axios.post('/api/credentials/pinterest-app', { clientId, clientSecret })
  129. pinterestCredentials.value = { configured: true, clientId, clientSecretHint: `****${clientSecret.slice(-4)}` }
  130. } catch (err: any) {
  131. pinterestError.value = err.response?.data?.error || 'Failed to save app credentials'
  132. } finally {
  133. pinterestLoading.value = false
  134. }
  135. }
  136. async function startPinterestOAuth() {
  137. pinterestLoading.value = true
  138. pinterestError.value = null
  139. try {
  140. const res = await axios.get('/api/auth/pinterest/init')
  141. window.location.href = res.data.url
  142. } catch (err: any) {
  143. pinterestError.value = err.response?.data?.error || 'Failed to start OAuth'
  144. pinterestLoading.value = false
  145. }
  146. }
  147. async function savePinterestBoards(selectedBoardIds: string[]) {
  148. pinterestLoading.value = true
  149. pinterestError.value = null
  150. try {
  151. await axios.post('/api/credentials/pinterest/boards', { selectedBoardIds })
  152. allPinterestBoards.value = allPinterestBoards.value.map((b) => ({
  153. ...b,
  154. selected: selectedBoardIds.includes(b.id),
  155. }))
  156. connectedPinterestBoards.value = allPinterestBoards.value.filter((b) => b.selected)
  157. } catch (err: any) {
  158. pinterestError.value = err.response?.data?.error || 'Failed to save board selection'
  159. } finally {
  160. pinterestLoading.value = false
  161. }
  162. }
  163. async function disconnectPinterest() {
  164. pinterestLoading.value = true
  165. try {
  166. await axios.delete('/api/credentials/pinterest')
  167. connectedPinterestBoards.value = []
  168. allPinterestBoards.value = []
  169. await fetchStatuses()
  170. } catch (err) {
  171. console.error('Pinterest disconnect error:', err)
  172. } finally {
  173. pinterestLoading.value = false
  174. }
  175. }
  176. // ─── Platform status ──────────────────────────────────────────────────────
  177. async function fetchStatuses() {
  178. loading.value = true
  179. try {
  180. const res = await axios.get('/feeds/platform-status')
  181. statuses.value = res.data
  182. } catch (err) {
  183. console.error('Platform status error:', err)
  184. } finally {
  185. loading.value = false
  186. }
  187. }
  188. function getStatus(platform: string): PlatformStatus | undefined {
  189. return statuses.value.find((s: PlatformStatus) => s.platform === platform)
  190. }
  191. function isConnected(platform: string): boolean {
  192. return getStatus(platform)?.connected ?? false
  193. }
  194. // ─── Meta App credentials ─────────────────────────────────────────────────
  195. async function fetchMetaCredentials() {
  196. try {
  197. const res = await axios.get('/api/credentials/meta-app')
  198. metaCredentials.value = res.data
  199. } catch (err) {
  200. console.error('Meta credentials fetch error:', err)
  201. }
  202. }
  203. async function saveMetaApp(appId: string, appSecret: string) {
  204. metaLoading.value = true
  205. metaError.value = null
  206. try {
  207. await axios.post('/api/credentials/meta-app', { appId, appSecret })
  208. metaCredentials.value = { configured: true, appId, appSecretHint: `****${appSecret.slice(-4)}` }
  209. } catch (err: any) {
  210. metaError.value = err.response?.data?.error || 'Failed to save app credentials'
  211. } finally {
  212. metaLoading.value = false
  213. }
  214. }
  215. // ─── OAuth flow ───────────────────────────────────────────────────────────
  216. async function startMetaOAuth() {
  217. metaLoading.value = true
  218. metaError.value = null
  219. try {
  220. const res = await axios.get('/api/auth/meta/init')
  221. // Redirect the browser to Facebook OAuth
  222. window.location.href = res.data.url
  223. } catch (err: any) {
  224. metaError.value = err.response?.data?.error || 'Failed to start OAuth'
  225. metaLoading.value = false
  226. }
  227. }
  228. async function fetchMetaDiscovery() {
  229. try {
  230. const res = await axios.get('/api/auth/meta/discovered')
  231. metaDiscovery.value = res.data
  232. } catch (err) {
  233. console.error('Meta discovery fetch error:', err)
  234. }
  235. }
  236. async function saveMetaSelection(selectedPageIds: string[], selectedIgAccountIds: string[]) {
  237. metaLoading.value = true
  238. metaError.value = null
  239. try {
  240. await axios.post('/api/auth/meta/save', { selectedPageIds, selectedIgAccountIds })
  241. metaDiscovery.value = null
  242. await fetchStatuses()
  243. } catch (err: any) {
  244. metaError.value = err.response?.data?.error || 'Failed to save selection'
  245. } finally {
  246. metaLoading.value = false
  247. }
  248. }
  249. async function disconnectMeta() {
  250. metaLoading.value = true
  251. try {
  252. await axios.delete('/api/credentials/meta')
  253. await fetchStatuses()
  254. } catch (err) {
  255. console.error('Meta disconnect error:', err)
  256. } finally {
  257. metaLoading.value = false
  258. }
  259. }
  260. return {
  261. statuses, loading, fetchStatuses, getStatus, isConnected,
  262. metaCredentials, metaDiscovery, metaLoading, metaError,
  263. connectedPages, connectedIgAccounts, fetchMetaConnections,
  264. fetchMetaCredentials, saveMetaApp, startMetaOAuth,
  265. fetchMetaDiscovery, saveMetaSelection, disconnectMeta,
  266. tokenExpiry, expiringAccounts, hasExpiryWarning,
  267. fetchTokenExpiry, dismissTokenWarning, refreshMetaTokens,
  268. pinterestCredentials, pinterestLoading, pinterestError,
  269. connectedPinterestBoards, allPinterestBoards,
  270. fetchPinterestCredentials, savePinterestApp, startPinterestOAuth,
  271. savePinterestBoards, disconnectPinterest,
  272. }
  273. })