diff --git a/docs/mammon-api.yml b/docs/mammon-api.yml index d8f7624..20b7923 100644 --- a/docs/mammon-api.yml +++ b/docs/mammon-api.yml @@ -325,6 +325,25 @@ paths: type: array items: $ref: "#/components/schemas/ReprocessingResultResponse" + /refresh: + post: + tags: + - auth + summary: Rotate the refresh session and mint a fresh access token + operationId: refresh + responses: + "200": + description: A fresh access token; the refresh cookie is rotated + content: + '*/*': + schema: + $ref: "#/components/schemas/RefreshResponse" + "401": + description: "Missing, expired, or reused refresh token" + content: + '*/*': + schema: + $ref: "#/components/schemas/RefreshResponse" /market/types/parse: post: tags: @@ -353,6 +372,15 @@ paths: "400": description: Returned when a line carries a quantity that is not a positive whole number + /logout: + post: + tags: + - auth + summary: Revoke the refresh session and clear the refresh cookie + operationId: logout + responses: + "204": + description: The refresh session is revoked and the cookie cleared /ledgers/main: post: tags: @@ -412,6 +440,18 @@ paths: "400": description: "Invalid request (e.g. blank name, a member ledger missing\ \ or already contained)" + /characters/add: + post: + tags: + - auth + summary: Seed the current user in the session so the next SSO links the new + character to them + operationId: addCharacter + responses: + "204": + description: The link is seeded; follow with a full-page nav to /oauth2/authorization/esi + "401": + description: Missing or expired access token /activity/fetch: post: tags: @@ -462,6 +502,25 @@ paths: text/plain: schema: type: string + /me: + get: + tags: + - auth + summary: Return the authenticated user and the characters they own + operationId: me + responses: + "200": + description: The authenticated user and their characters + content: + '*/*': + schema: + $ref: "#/components/schemas/MeResponse" + "401": + description: Missing or expired access token + content: + '*/*': + schema: + $ref: "#/components/schemas/MeResponse" /market/{marketTypeId}/scan: get: tags: @@ -923,18 +982,28 @@ components: schemas: UpdateRuleBookRequest: type: object + description: Request to update an existing rule book; replaces its mutable fields. properties: name: type: string + description: Rule book name. + example: Trading rules usedForAcquisitions: type: boolean + description: Whether this rule book is used to derive acquisitions. + example: true ledgerRefs: type: array + description: Symbolic ledger references the script writes to; each must + be bound to a ledger per character. items: type: string + example: sales pattern: "[a-z][a-zA-Z0-9]*" script: type: string + description: The classification script source. + example: transfer.to('sales') required: - ledgerRefs - name @@ -942,21 +1011,34 @@ components: - usedForAcquisitions RuleBookResponse: type: object + description: "A rule book: a named script that classifies transactions, together\ + \ with the ledger references it expects." properties: ruleBookId: type: string format: uuid + description: Unique rule book identifier. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 name: type: string + description: Rule book name. + example: Trading rules usedForAcquisitions: type: boolean + description: Whether this rule book is used to derive acquisitions. + example: true ledgerRefs: type: array + description: Symbolic ledger references the script writes to; each must + be bound to a ledger per character. items: type: string + example: sales pattern: "[a-z][a-zA-Z0-9]*" script: type: string + description: The classification script source. + example: transfer.to('sales') required: - ledgerRefs - name @@ -965,9 +1047,12 @@ components: - usedForAcquisitions UpdateMainLedgerRequest: type: object + description: Request to update an existing main ledger. properties: name: type: string + description: New name for the ledger. + example: Main wallet required: - name MainLedgerResponse: @@ -978,21 +1063,33 @@ components: ledgerId: type: string format: uuid + description: Unique ledger identifier. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 name: type: string + description: Ledger name. + example: Main wallet balance: type: number + description: Current ISK balance of the ledger. + example: 1500000.0 + description: "A main ledger: a standalone ledger with its own balance." required: - balance - ledgerId - name UpdateCombinedLedgerRequest: type: object + description: Request to update an existing combined ledger; replaces its name + and member set. properties: name: type: string + description: New name for the combined ledger. + example: All wallets memberLedgerIds: type: array + description: Ids of the ledgers to aggregate. items: type: string format: uuid @@ -1007,15 +1104,24 @@ components: ledgerId: type: string format: uuid + description: Unique ledger identifier. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 name: type: string + description: Ledger name. + example: All wallets balance: type: number + description: Aggregate ISK balance across all member ledgers. + example: 1500000.0 memberLedgerIds: type: array + description: Ids of the ledgers aggregated by this combined ledger. items: type: string format: uuid + description: "A combined ledger: an aggregate whose balance is the sum of its\ + \ member ledgers." required: - balance - ledgerId @@ -1023,70 +1129,106 @@ components: - name SetCharacterRuleBookRequest: type: object + description: "Request to assign a rule book to a character, binding each of\ + \ its ledger references to a concrete ledger." properties: ruleBookId: type: string format: uuid + description: Id of the rule book to assign. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 bindings: type: object additionalProperties: type: string format: uuid + description: Bindings from each ledger reference name to the ledger id it + resolves to for this character. + example: + sales: 5c1e6f2a-1234-4562-b3fc-2c963f66afa6 required: - bindings - ruleBookId CharacterResponse: type: object + description: An EVE Online character. properties: characterId: type: integer format: int64 + description: EVE character id. + example: 2112625428 name: type: string + description: Character name. + example: CCP Falcon required: - characterId - name CharacterRuleBookResponse: type: object + description: "A rule book assigned to a character, with the ledger bindings\ + \ that resolve its references." properties: character: $ref: "#/components/schemas/CharacterResponse" + description: The character the rule book is assigned to. ruleBook: $ref: "#/components/schemas/RuleBookSummaryResponse" + description: Summary of the assigned rule book. bindings: type: object additionalProperties: type: string format: uuid + description: Bindings from each ledger reference name to the ledger id it + resolves to for this character. + example: + sales: 5c1e6f2a-1234-4562-b3fc-2c963f66afa6 required: - bindings - character - ruleBook RuleBookSummaryResponse: type: object + description: "Lightweight reference to a rule book: its id and name only." properties: ruleBookId: type: string format: uuid + description: Unique rule book identifier. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 name: type: string + description: Rule book name. + example: Trading rules required: - name - ruleBookId CreateRuleBookRequest: type: object + description: Request to create a new rule book. properties: name: type: string + description: Rule book name. + example: Trading rules usedForAcquisitions: type: boolean + description: Whether this rule book is used to derive acquisitions. + example: true ledgerRefs: type: array + description: Symbolic ledger references the script writes to; each must + be bound to a ledger per character. items: type: string + example: sales pattern: "[a-z][a-zA-Z0-9]*" script: type: string + description: The classification script source. + example: transfer.to('sales') required: - ledgerRefs - name @@ -1094,24 +1236,34 @@ components: - usedForAcquisitions MarketTypeStackRequest: type: object + description: A quantity of a single market type to reprocess. properties: marketTypeId: type: integer format: int64 + description: EVE market type (item) id to reprocess. + example: 1230 quantity: type: integer format: int64 + description: Number of units to reprocess. + example: 1000 required: - marketTypeId - quantity ReprocessItemsRequest: type: object + description: Request to reprocess a set of item stacks on behalf of a character. properties: characterId: type: integer format: int64 + description: EVE character id whose reprocessing skills and facility standings + are applied. + example: 2112625428 items: type: array + description: Item stacks to reprocess. items: $ref: "#/components/schemas/MarketTypeStackRequest" required: @@ -1119,20 +1271,31 @@ components: - items MarketTypeStackAppraisalResponse: type: object + description: A market type stack valued at current buy and sell prices. properties: marketTypeId: type: integer format: int64 + description: EVE market type (item) id. + example: 34 quantity: type: integer format: int64 + description: Number of units in the stack. + example: 1000 buy: type: number + description: "Total buy value of the stack, in ISK." + example: 5420.0 sell: type: number + description: "Total sell value of the stack, in ISK." + example: 6100.0 orderCount: type: integer format: int64 + description: Number of market orders considered for the valuation. + example: 128 required: - buy - marketTypeId @@ -1141,77 +1304,142 @@ components: - sell ReprocessingResultResponse: type: object + description: "Result of reprocessing an item stack: the input stack and the\ + \ resulting material outputs, each valued at market prices." properties: input: $ref: "#/components/schemas/MarketTypeStackAppraisalResponse" + description: The input stack that was reprocessed. output: type: array + description: Material stacks yielded by reprocessing the input. items: $ref: "#/components/schemas/MarketTypeStackAppraisalResponse" required: - input - output + RefreshResponse: + type: object + description: A freshly minted access token. + properties: + accessToken: + type: string + description: Short-lived bearer access token (JWT) for the Authorization + header. + required: + - accessToken MarketTypeStackResponse: type: object + description: A quantity of a single market type. properties: marketTypeId: type: integer format: int64 + description: EVE market type (item) id. + example: 34 quantity: type: integer format: int64 + description: Number of units in the stack. + example: 1000 required: - marketTypeId - quantity CreateMainLedgerRequest: type: object + description: Request to create a new main ledger. properties: name: type: string + description: Name for the new ledger. + example: Main wallet required: - name CreateCombinedLedgerRequest: type: object + description: Request to create a new combined ledger aggregating the given member + ledgers. properties: name: type: string + description: Name for the new combined ledger. + example: All wallets memberLedgerIds: type: array + description: Ids of the ledgers to aggregate. items: type: string format: uuid required: - memberLedgerIds - name + MeResponse: + type: object + description: The authenticated user and the characters they own. + properties: + userId: + type: string + format: uuid + description: Unique user identifier. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 + characters: + type: array + description: Characters owned by the user. + items: + $ref: "#/components/schemas/CharacterResponse" + required: + - characters + - userId MarketScanResponse: type: object + description: "Result of a market scan combining current prices, history quartiles\ + \ and a computed profitability score for a market type." properties: marketTypeId: type: integer format: int64 + description: EVE market type (item) id. + example: 34 buy: type: number + description: "Current best buy price, in ISK." + example: 5.42 sell: type: number + description: "Current best sell price, in ISK." + example: 6.1 q1: type: number + description: "First quartile (25th percentile) historical price, in ISK." + example: 4.8 median: type: number + description: "Median (50th percentile) historical price, in ISK." + example: 5.42 q3: type: number + description: "Third quartile (75th percentile) historical price, in ISK." + example: 6.05 totalVolume: type: integer format: int64 + description: Total traded volume over the scanned period. + example: 15000000 trend: type: string + description: Overall price trend over the scanned period. enum: - UP - DOWN - FLAT profit: type: number + description: "Estimated profit per unit after fees and taxes, in ISK." + example: 0.45 score: type: number + description: Computed profitability score used to rank scan results. + example: 0.82 required: - buy - marketTypeId @@ -1225,25 +1453,40 @@ components: - trend MarketHistoryResponse: type: object + description: A single day of market history for a market type. properties: marketTypeId: type: integer format: int64 + description: EVE market type (item) id. + example: 34 date: type: string format: date + description: Date the history entry covers. + example: 2026-06-22 average: type: number + description: "Average price for the day, in ISK." + example: 5.42 highest: type: number + description: "Highest price for the day, in ISK." + example: 6.1 lowest: type: number + description: "Lowest price for the day, in ISK." + example: 4.8 orderCount: type: integer format: int64 + description: Number of orders for the day. + example: 320 volume: type: integer format: int64 + description: Traded volume for the day. + example: 12500000 required: - average - date @@ -1254,21 +1497,33 @@ components: - volume HistoryQuartilesResponse: type: object + description: Quartile statistics computed over a market type's price history. properties: marketTypeId: type: integer format: int64 + description: EVE market type (item) id. + example: 34 q1: type: number + description: "First quartile (25th percentile) price, in ISK." + example: 4.8 median: type: number + description: "Median (50th percentile) price, in ISK." + example: 5.42 q3: type: number + description: "Third quartile (75th percentile) price, in ISK." + example: 6.05 totalVolume: type: integer format: int64 + description: Total traded volume over the period. + example: 15000000 trend: type: string + description: Overall price trend over the period. enum: - UP - DOWN @@ -1282,33 +1537,58 @@ components: - trend MarketTypeResponse: type: object + description: An EVE Online market type (item) and its static attributes. properties: id: type: integer format: int64 + description: EVE market type (item) id. + example: 34 name: type: string + description: Item name. + example: Tritanium groupId: type: integer format: int64 + description: Group id the item belongs to. + example: 18 marketGroupId: type: integer format: int64 + description: Market group id the item is listed under. + example: 1857 description: type: string + description: Item description. + example: The main building block in space structures. published: type: boolean + description: Whether the item is published and visible in game. + example: true basePrice: - type: number + type: + - number + - "null" + description: "Base price of the item, in ISK, if defined." + example: 2.99 volume: type: number format: double + description: "Packaged volume of a single unit, in m³." + example: 0.01 portionSize: type: integer format: int32 + description: Number of units produced or reprocessed per portion. + example: 1 iconId: - type: integer + type: + - integer + - "null" format: int64 + description: "Icon id for the item, if defined." + example: 1 required: - basePrice - description @@ -1322,23 +1602,34 @@ components: - volume MarketPriceResponse: type: object + description: Current best buy and sell prices for a market type. properties: marketTypeId: type: integer format: int64 + description: EVE market type (item) id. + example: 34 buy: type: number + description: "Best buy price, in ISK." + example: 5.42 sell: type: number + description: "Best sell price, in ISK." + example: 6.1 orderCount: type: integer format: int64 + description: Number of market orders considered. + example: 128 required: - buy - marketTypeId - orderCount - sell LedgerResponse: + description: "A ledger, either a standalone main ledger or a combined ledger\ + \ aggregating others." discriminator: propertyName: type mapping: @@ -1360,11 +1651,18 @@ components: fromLedgerId: type: string format: uuid + description: Id of the ledger the ISK is debited from. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 toLedgerId: type: string format: uuid + description: Id of the ledger the ISK is credited to. + example: 5c1e6f2a-1234-4562-b3fc-2c963f66afa6 amount: type: number + description: Amount of ISK transferred. + example: 1500000.0 + description: A transfer of ISK from one ledger to another. required: - amount - fromLedgerId @@ -1377,15 +1675,24 @@ components: fromLedgerId: type: string format: uuid + description: Id of the ledger the items are debited from. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 toLedgerId: type: string format: uuid + description: Id of the ledger the items are credited to. + example: 5c1e6f2a-1234-4562-b3fc-2c963f66afa6 marketTypeId: type: integer format: int64 + description: EVE market type (item) id transferred. + example: 34 quantity: type: integer format: int64 + description: Number of units transferred. + example: 1000 + description: A transfer of an item stack from one ledger to another. required: - fromLedgerId - marketTypeId @@ -1393,20 +1700,32 @@ components: - toLedgerId TransactionResponse: type: object + description: "A transaction: a dated, described group of transfers between ledgers." properties: transactionId: type: string format: uuid + description: Unique transaction identifier. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 characterId: - type: integer + type: + - integer + - "null" format: int64 + description: "EVE character id the transaction is attributed to, if any." + example: 2112625428 datetime: type: string format: date-time + description: When the transaction occurred. + example: 2026-06-22T14:30:00 description: type: string + description: Human-readable description of the transaction. + example: Market sale of Tritanium transfers: type: array + description: The transfers that make up this transaction. items: $ref: "#/components/schemas/TransferResponse" required: @@ -1416,6 +1735,7 @@ components: - transactionId - transfers TransferResponse: + description: "A transfer between two ledgers, either of ISK or of an item stack." discriminator: propertyName: type mapping: @@ -1431,11 +1751,16 @@ components: - type BalanceResponse: type: object + description: "The balance of a ledger: its ISK total plus the quantity of each\ + \ item type held." properties: iskBalance: type: number + description: ISK balance of the ledger. + example: 1500000.0 itemBalances: type: array + description: Per-item-type quantities held in the ledger. items: $ref: "#/components/schemas/ItemBalanceResponse" required: @@ -1443,44 +1768,66 @@ components: - itemBalances ItemBalanceResponse: type: object + description: Quantity of a single item type held in a ledger. properties: typeId: type: integer format: int64 + description: EVE market type (item) id. + example: 34 quantity: type: integer format: int64 + description: Number of units held. + example: 1000 required: - quantity - typeId AcquisitionResponse: type: object + description: "A single acquisition of a market type, tracking the remaining\ + \ quantity available in the FIFO cost pool." properties: acquisitionId: type: string format: uuid + description: Unique acquisition identifier. + example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 characterId: type: integer format: int64 + description: EVE character id that made the acquisition. + example: 2112625428 marketTypeId: type: integer format: int64 + description: EVE market type (item) id that was acquired. + example: 34 source: type: string + description: How the item entered the inventory. enum: - BOUGHT - MANUAL datetime: type: string format: date-time + description: When the acquisition occurred. + example: 2026-06-22T14:30:00 quantity: type: integer format: int64 + description: Quantity originally acquired. + example: 1000 remaining: type: integer format: int64 + description: Quantity still remaining in the FIFO cost pool. + example: 250 unitCost: type: number + description: "Cost per unit at acquisition time, in ISK." + example: 5.42 required: - acquisitionId - characterId diff --git a/src/auth/auth.ts b/src/auth/auth.ts new file mode 100644 index 0000000..93bd4f2 --- /dev/null +++ b/src/auth/auth.ts @@ -0,0 +1,67 @@ +import {defineStore} from "pinia"; +import {ref} from "vue"; +import {CharacterResponse} from "@/generated/mammon"; +import {addCharacter as addCharacterRequest, fetchMe, mammonLoginUrl, postLogout, refreshAccessToken, setOnAuthExpired} from "@/mammon"; +import {setAccessToken} from "./token"; + +export const useAuthStore = defineStore('auth', () => { + const userId = ref(null); + const characters = ref([]); + const isAuthenticated = ref(false); + + const clear = () => { + setAccessToken(null); + userId.value = null; + characters.value = []; + isAuthenticated.value = false; + } + + const refresh = async (): Promise => { + const token = await refreshAccessToken(); + + if (!token) { + clear(); + return false; + } + isAuthenticated.value = true; + return true; + } + + const fetch = async (): Promise => { + const me = await fetchMe(); + + userId.value = me.userId; + characters.value = me.characters; + isAuthenticated.value = true; + } + + const login = (): void => { + window.location.assign(mammonLoginUrl); + } + + const addCharacter = async (): Promise => { + await addCharacterRequest(); + window.location.assign(mammonLoginUrl); + } + + const logout = async (): Promise => { + try { + await postLogout(); + } finally { + clear(); + } + } + + const bootstrap = async (): Promise => { + setOnAuthExpired(() => { + clear(); + login(); + }); + + if (await refresh()) { + await fetch(); + } + } + + return {userId, characters, isAuthenticated, refresh, fetch, login, addCharacter, logout, bootstrap}; +}) \ No newline at end of file diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..43fd736 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,2 @@ +export * from './auth'; +export * from './token'; \ No newline at end of file diff --git a/src/auth/token.ts b/src/auth/token.ts new file mode 100644 index 0000000..f7e0d49 --- /dev/null +++ b/src/auth/token.ts @@ -0,0 +1,7 @@ +let accessToken: string | null = null; + +export const getAccessToken = (): string | null => accessToken; + +export const setAccessToken = (token: string | null): void => { + accessToken = token; +}; \ No newline at end of file diff --git a/src/generated/mammon/api.ts b/src/generated/mammon/api.ts index 68d43dd..9035e0b 100644 --- a/src/generated/mammon/api.ts +++ b/src/generated/mammon/api.ts @@ -23,14 +23,41 @@ import type { RequestArgs } from './base'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base'; +/** + * A single acquisition of a market type, tracking the remaining quantity available in the FIFO cost pool. + */ export interface AcquisitionResponse { + /** + * Unique acquisition identifier. + */ 'acquisitionId': string; + /** + * EVE character id that made the acquisition. + */ 'characterId': number; + /** + * EVE market type (item) id that was acquired. + */ 'marketTypeId': number; + /** + * How the item entered the inventory. + */ 'source': AcquisitionResponseSourceEnum; + /** + * When the acquisition occurred. + */ 'datetime': string; + /** + * Quantity originally acquired. + */ 'quantity': number; + /** + * Quantity still remaining in the FIFO cost pool. + */ 'remaining': number; + /** + * Cost per unit at acquisition time, in ISK. + */ 'unitCost': number; } @@ -41,44 +68,140 @@ export const AcquisitionResponseSourceEnum = { export type AcquisitionResponseSourceEnum = typeof AcquisitionResponseSourceEnum[keyof typeof AcquisitionResponseSourceEnum]; +/** + * The balance of a ledger: its ISK total plus the quantity of each item type held. + */ export interface BalanceResponse { + /** + * ISK balance of the ledger. + */ 'iskBalance': number; + /** + * Per-item-type quantities held in the ledger. + */ 'itemBalances': Array; } +/** + * An EVE Online character. + */ export interface CharacterResponse { + /** + * EVE character id. + */ 'characterId': number; + /** + * Character name. + */ 'name': string; } +/** + * A rule book assigned to a character, with the ledger bindings that resolve its references. + */ export interface CharacterRuleBookResponse { + /** + * The character the rule book is assigned to. + */ 'character': CharacterResponse; + /** + * Summary of the assigned rule book. + */ 'ruleBook': RuleBookSummaryResponse; + /** + * Bindings from each ledger reference name to the ledger id it resolves to for this character. + */ 'bindings': { [key: string]: string; }; } +/** + * A combined ledger: an aggregate whose balance is the sum of its member ledgers. + */ export interface CombinedLedgerResponse extends LedgerResponse { + /** + * Unique ledger identifier. + */ 'ledgerId': string; + /** + * Ledger name. + */ 'name': string; + /** + * Aggregate ISK balance across all member ledgers. + */ 'balance': number; + /** + * Ids of the ledgers aggregated by this combined ledger. + */ 'memberLedgerIds': Array; } +/** + * Request to create a new combined ledger aggregating the given member ledgers. + */ export interface CreateCombinedLedgerRequest { + /** + * Name for the new combined ledger. + */ 'name': string; + /** + * Ids of the ledgers to aggregate. + */ 'memberLedgerIds': Array; } +/** + * Request to create a new main ledger. + */ export interface CreateMainLedgerRequest { + /** + * Name for the new ledger. + */ 'name': string; } +/** + * Request to create a new rule book. + */ export interface CreateRuleBookRequest { + /** + * Rule book name. + */ 'name': string; + /** + * Whether this rule book is used to derive acquisitions. + */ 'usedForAcquisitions': boolean; + /** + * Symbolic ledger references the script writes to; each must be bound to a ledger per character. + */ 'ledgerRefs': Array; + /** + * The classification script source. + */ 'script': string; } +/** + * Quartile statistics computed over a market type\'s price history. + */ export interface HistoryQuartilesResponse { + /** + * EVE market type (item) id. + */ 'marketTypeId': number; + /** + * First quartile (25th percentile) price, in ISK. + */ 'q1': number; + /** + * Median (50th percentile) price, in ISK. + */ 'median': number; + /** + * Third quartile (75th percentile) price, in ISK. + */ 'q3': number; + /** + * Total traded volume over the period. + */ 'totalVolume': number; + /** + * Overall price trend over the period. + */ 'trend': HistoryQuartilesResponseTrendEnum; } @@ -90,56 +213,177 @@ export const HistoryQuartilesResponseTrendEnum = { export type HistoryQuartilesResponseTrendEnum = typeof HistoryQuartilesResponseTrendEnum[keyof typeof HistoryQuartilesResponseTrendEnum]; +/** + * A transfer of ISK from one ledger to another. + */ export interface IskTransferResponse extends TransferResponse { + /** + * Id of the ledger the ISK is debited from. + */ 'fromLedgerId': string; + /** + * Id of the ledger the ISK is credited to. + */ 'toLedgerId': string; + /** + * Amount of ISK transferred. + */ 'amount': number; } +/** + * Quantity of a single item type held in a ledger. + */ export interface ItemBalanceResponse { + /** + * EVE market type (item) id. + */ 'typeId': number; + /** + * Number of units held. + */ 'quantity': number; } +/** + * A transfer of an item stack from one ledger to another. + */ export interface ItemTransferResponse extends TransferResponse { + /** + * Id of the ledger the items are debited from. + */ 'fromLedgerId': string; + /** + * Id of the ledger the items are credited to. + */ 'toLedgerId': string; + /** + * EVE market type (item) id transferred. + */ 'marketTypeId': number; + /** + * Number of units transferred. + */ 'quantity': number; } /** * @type LedgerResponse + * A ledger, either a standalone main ledger or a combined ledger aggregating others. */ export type LedgerResponse = { type: 'COMBINED' } & CombinedLedgerResponse | { type: 'MAIN' } & MainLedgerResponse; +/** + * A main ledger: a standalone ledger with its own balance. + */ export interface MainLedgerResponse extends LedgerResponse { + /** + * Unique ledger identifier. + */ 'ledgerId': string; + /** + * Ledger name. + */ 'name': string; + /** + * Current ISK balance of the ledger. + */ 'balance': number; } +/** + * A single day of market history for a market type. + */ export interface MarketHistoryResponse { + /** + * EVE market type (item) id. + */ 'marketTypeId': number; + /** + * Date the history entry covers. + */ 'date': string; + /** + * Average price for the day, in ISK. + */ 'average': number; + /** + * Highest price for the day, in ISK. + */ 'highest': number; + /** + * Lowest price for the day, in ISK. + */ 'lowest': number; + /** + * Number of orders for the day. + */ 'orderCount': number; + /** + * Traded volume for the day. + */ 'volume': number; } +/** + * Current best buy and sell prices for a market type. + */ export interface MarketPriceResponse { + /** + * EVE market type (item) id. + */ 'marketTypeId': number; + /** + * Best buy price, in ISK. + */ 'buy': number; + /** + * Best sell price, in ISK. + */ 'sell': number; + /** + * Number of market orders considered. + */ 'orderCount': number; } +/** + * Result of a market scan combining current prices, history quartiles and a computed profitability score for a market type. + */ export interface MarketScanResponse { + /** + * EVE market type (item) id. + */ 'marketTypeId': number; + /** + * Current best buy price, in ISK. + */ 'buy': number; + /** + * Current best sell price, in ISK. + */ 'sell': number; + /** + * First quartile (25th percentile) historical price, in ISK. + */ 'q1': number; + /** + * Median (50th percentile) historical price, in ISK. + */ 'median': number; + /** + * Third quartile (75th percentile) historical price, in ISK. + */ 'q3': number; + /** + * Total traded volume over the scanned period. + */ 'totalVolume': number; + /** + * Overall price trend over the scanned period. + */ 'trend': MarketScanResponseTrendEnum; + /** + * Estimated profit per unit after fees and taxes, in ISK. + */ 'profit': number; + /** + * Computed profitability score used to rank scan results. + */ 'score': number; } @@ -151,79 +395,273 @@ export const MarketScanResponseTrendEnum = { export type MarketScanResponseTrendEnum = typeof MarketScanResponseTrendEnum[keyof typeof MarketScanResponseTrendEnum]; +/** + * An EVE Online market type (item) and its static attributes. + */ export interface MarketTypeResponse { + /** + * EVE market type (item) id. + */ 'id': number; + /** + * Item name. + */ 'name': string; + /** + * Group id the item belongs to. + */ 'groupId': number; + /** + * Market group id the item is listed under. + */ 'marketGroupId': number; + /** + * Item description. + */ 'description': string; + /** + * Whether the item is published and visible in game. + */ 'published': boolean; - 'basePrice': number; + /** + * Base price of the item, in ISK, if defined. + */ + 'basePrice': number | null; + /** + * Packaged volume of a single unit, in m³. + */ 'volume': number; + /** + * Number of units produced or reprocessed per portion. + */ 'portionSize': number; - 'iconId': number; + /** + * Icon id for the item, if defined. + */ + 'iconId': number | null; } +/** + * A market type stack valued at current buy and sell prices. + */ export interface MarketTypeStackAppraisalResponse { + /** + * EVE market type (item) id. + */ 'marketTypeId': number; + /** + * Number of units in the stack. + */ 'quantity': number; + /** + * Total buy value of the stack, in ISK. + */ 'buy': number; + /** + * Total sell value of the stack, in ISK. + */ 'sell': number; + /** + * Number of market orders considered for the valuation. + */ 'orderCount': number; } +/** + * A quantity of a single market type to reprocess. + */ export interface MarketTypeStackRequest { + /** + * EVE market type (item) id to reprocess. + */ 'marketTypeId': number; + /** + * Number of units to reprocess. + */ 'quantity': number; } +/** + * A quantity of a single market type. + */ export interface MarketTypeStackResponse { + /** + * EVE market type (item) id. + */ 'marketTypeId': number; + /** + * Number of units in the stack. + */ 'quantity': number; } +/** + * The authenticated user and the characters they own. + */ +export interface MeResponse { + /** + * Unique user identifier. + */ + 'userId': string; + /** + * Characters owned by the user. + */ + 'characters': Array; +} +/** + * A freshly minted access token. + */ +export interface RefreshResponse { + /** + * Short-lived bearer access token (JWT) for the Authorization header. + */ + 'accessToken': string; +} +/** + * Request to reprocess a set of item stacks on behalf of a character. + */ export interface ReprocessItemsRequest { + /** + * EVE character id whose reprocessing skills and facility standings are applied. + */ 'characterId': number; + /** + * Item stacks to reprocess. + */ 'items': Array; } +/** + * Result of reprocessing an item stack: the input stack and the resulting material outputs, each valued at market prices. + */ export interface ReprocessingResultResponse { + /** + * The input stack that was reprocessed. + */ 'input': MarketTypeStackAppraisalResponse; + /** + * Material stacks yielded by reprocessing the input. + */ 'output': Array; } +/** + * A rule book: a named script that classifies transactions, together with the ledger references it expects. + */ export interface RuleBookResponse { + /** + * Unique rule book identifier. + */ 'ruleBookId': string; + /** + * Rule book name. + */ 'name': string; + /** + * Whether this rule book is used to derive acquisitions. + */ 'usedForAcquisitions': boolean; + /** + * Symbolic ledger references the script writes to; each must be bound to a ledger per character. + */ 'ledgerRefs': Array; + /** + * The classification script source. + */ 'script': string; } +/** + * Lightweight reference to a rule book: its id and name only. + */ export interface RuleBookSummaryResponse { + /** + * Unique rule book identifier. + */ 'ruleBookId': string; + /** + * Rule book name. + */ 'name': string; } +/** + * Request to assign a rule book to a character, binding each of its ledger references to a concrete ledger. + */ export interface SetCharacterRuleBookRequest { + /** + * Id of the rule book to assign. + */ 'ruleBookId': string; + /** + * Bindings from each ledger reference name to the ledger id it resolves to for this character. + */ 'bindings': { [key: string]: string; }; } +/** + * A transaction: a dated, described group of transfers between ledgers. + */ export interface TransactionResponse { + /** + * Unique transaction identifier. + */ 'transactionId': string; - 'characterId': number; + /** + * EVE character id the transaction is attributed to, if any. + */ + 'characterId': number | null; + /** + * When the transaction occurred. + */ 'datetime': string; + /** + * Human-readable description of the transaction. + */ 'description': string; + /** + * The transfers that make up this transaction. + */ 'transfers': Array; } /** * @type TransferResponse + * A transfer between two ledgers, either of ISK or of an item stack. */ export type TransferResponse = { type: 'ISK' } & IskTransferResponse | { type: 'ITEM' } & ItemTransferResponse; +/** + * Request to update an existing combined ledger; replaces its name and member set. + */ export interface UpdateCombinedLedgerRequest { + /** + * New name for the combined ledger. + */ 'name': string; + /** + * Ids of the ledgers to aggregate. + */ 'memberLedgerIds': Array; } +/** + * Request to update an existing main ledger. + */ export interface UpdateMainLedgerRequest { + /** + * New name for the ledger. + */ 'name': string; } +/** + * Request to update an existing rule book; replaces its mutable fields. + */ export interface UpdateRuleBookRequest { + /** + * Rule book name. + */ 'name': string; + /** + * Whether this rule book is used to derive acquisitions. + */ 'usedForAcquisitions': boolean; + /** + * Symbolic ledger references the script writes to; each must be bound to a ledger per character. + */ 'ledgerRefs': Array; + /** + * The classification script source. + */ 'script': string; } @@ -557,6 +995,281 @@ export class ActivityApi extends BaseAPI { +/** + * AuthApi - axios parameter creator + */ +export const AuthApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Seed the current user in the session so the next SSO links the new character to them + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + addCharacter: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/characters/add`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Revoke the refresh session and clear the refresh cookie + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + logout: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/logout`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Return the authenticated user and the characters they own + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + me: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Accept'] = '*/*'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Rotate the refresh session and mint a fresh access token + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + refresh: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/refresh`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Accept'] = '*/*'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AuthApi - functional programming interface + */ +export const AuthApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AuthApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Seed the current user in the session so the next SSO links the new character to them + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async addCharacter(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.addCharacter(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AuthApi.addCharacter']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Revoke the refresh session and clear the refresh cookie + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async logout(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.logout(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AuthApi.logout']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Return the authenticated user and the characters they own + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async me(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.me(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AuthApi.me']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Rotate the refresh session and mint a fresh access token + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async refresh(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.refresh(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AuthApi.refresh']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * AuthApi - factory interface + */ +export const AuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AuthApiFp(configuration) + return { + /** + * + * @summary Seed the current user in the session so the next SSO links the new character to them + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + addCharacter(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.addCharacter(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Revoke the refresh session and clear the refresh cookie + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + logout(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.logout(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Return the authenticated user and the characters they own + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + me(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.me(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Rotate the refresh session and mint a fresh access token + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + refresh(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.refresh(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * AuthApi - object-oriented interface + */ +export class AuthApi extends BaseAPI { + /** + * + * @summary Seed the current user in the session so the next SSO links the new character to them + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public addCharacter(options?: RawAxiosRequestConfig) { + return AuthApiFp(this.configuration).addCharacter(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Revoke the refresh session and clear the refresh cookie + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public logout(options?: RawAxiosRequestConfig) { + return AuthApiFp(this.configuration).logout(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Return the authenticated user and the characters they own + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public me(options?: RawAxiosRequestConfig) { + return AuthApiFp(this.configuration).me(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Rotate the refresh session and mint a fresh access token + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public refresh(options?: RawAxiosRequestConfig) { + return AuthApiFp(this.configuration).refresh(options).then((request) => request(this.axios, this.basePath)); + } +} + + + /** * CharacterApi - axios parameter creator */ diff --git a/src/main.ts b/src/main.ts index 5c35c72..8cc01e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,10 @@ -import { createPinia } from 'pinia'; -import { createApp } from 'vue'; -import { createRouter, createWebHistory } from 'vue-router'; +import {createPinia} from 'pinia'; +import {createApp} from 'vue'; +import {createRouter, createWebHistory} from 'vue-router'; import App from './App.vue'; -import { initLogger } from './logger'; -import { routes } from './routes'; +import {useAuthStore} from './auth'; +import {initLogger} from './logger'; +import {routeNames, routes} from './routes'; import './style.css'; initLogger(); @@ -18,4 +19,13 @@ const router = createRouter({ app.use(pinia); app.use(router); -app.mount('#app'); +const authStore = useAuthStore(); + +router.beforeEach(to => { + if (!to.meta.public && !authStore.isAuthenticated) { + return {name: routeNames.home}; + } + return true; +}); + +authStore.bootstrap().finally(() => app.mount('#app')); \ No newline at end of file diff --git a/src/mammon/mammonService.ts b/src/mammon/mammonService.ts index adee236..dd043f1 100644 --- a/src/mammon/mammonService.ts +++ b/src/mammon/mammonService.ts @@ -1,19 +1,22 @@ import {logResource} from "@/service"; -import axios from "axios"; +import {getAccessToken, setAccessToken} from "@/auth/token"; +import axios, {InternalAxiosRequestConfig} from "axios"; import { AcquisitionApi, ActivityApi, + AuthApi, CharacterApi, CharacterRuleBookApi, LedgerApi, MarketApi, + MeResponse, ReprocessingApi, RuleBookApi, TransactionApi } from "@/generated/mammon"; export const mammonUrl = import.meta.env.VITE_MAMMON_URL; -export const mammonAddCharacterUrl = mammonUrl + "oauth2/authorization/esi" +export const mammonLoginUrl = mammonUrl + "oauth2/authorization/esi" const mammonAxiosInstance = axios.create({ baseURL: mammonUrl, @@ -24,6 +27,76 @@ const mammonAxiosInstance = axios.create({ }) logResource(mammonAxiosInstance) +const credentialedClient = axios.create({ + baseURL: mammonUrl, + withCredentials: true, +}) +logResource(credentialedClient) + +mammonAxiosInstance.interceptors.request.use(config => { + const token = getAccessToken(); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}) + +const authApi = new AuthApi(undefined, mammonUrl, mammonAxiosInstance); +const credentialedAuthApi = new AuthApi(undefined, mammonUrl, credentialedClient); + +let onAuthExpired: () => void = () => {}; + +export const setOnAuthExpired = (callback: () => void): void => { + onAuthExpired = callback; +} + +let refreshing: Promise | null = null; + +export const refreshAccessToken = (): Promise => { + if (!refreshing) { + refreshing = credentialedAuthApi.refresh() + .then(response => { + setAccessToken(response.data.accessToken); + return response.data.accessToken; + }) + .catch(() => { + setAccessToken(null); + return null; + }) + .finally(() => { + refreshing = null; + }); + } + return refreshing; +} + +mammonAxiosInstance.interceptors.response.use(response => response, async error => { + const original = error.config as (InternalAxiosRequestConfig & { _retried?: boolean }) | undefined; + + if (error.response?.status === 401 && original && !original._retried) { + original._retried = true; + + const token = await refreshAccessToken(); + + if (token) { + original.headers.Authorization = `Bearer ${token}`; + return mammonAxiosInstance(original); + } + onAuthExpired(); + } + return Promise.reject(error); +}) + +export const fetchMe = (): Promise => + authApi.me().then(response => response.data); + +export const addCharacter = (): Promise => + authApi.addCharacter({withCredentials: true}).then(() => undefined); + +export const postLogout = (): Promise => + credentialedAuthApi.logout().then(() => undefined); + export const ledgerApi = new LedgerApi(undefined, mammonUrl, mammonAxiosInstance); export const transactionApi = new TransactionApi(undefined, mammonUrl, mammonAxiosInstance); export const characterApi = new CharacterApi(undefined, mammonUrl, mammonAxiosInstance); @@ -32,4 +105,4 @@ export const characterRuleBookApi = new CharacterRuleBookApi(undefined, mammonUr export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInstance); export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance); export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance); -export const reprocessingApi = new ReprocessingApi(undefined, mammonUrl, mammonAxiosInstance); +export const reprocessingApi = new ReprocessingApi(undefined, mammonUrl, mammonAxiosInstance); \ No newline at end of file diff --git a/src/pages/Characters.vue b/src/pages/Characters.vue index 09d1b60..6719744 100644 --- a/src/pages/Characters.vue +++ b/src/pages/Characters.vue @@ -1,14 +1,13 @@