From 2bc251a58bfb12734c6cc7ca151760bebb109d48 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Tue, 5 Apr 2022 20:37:33 +0100 Subject: [PATCH] Add Nintendo Account authentication and web services in Electron --- .vscode/launch.json | 7 +- src/api/na.ts | 67 +++++++++ src/app/browser/app.tsx | 4 +- src/app/electron.ts | 18 ++- src/app/main/index.ts | 52 +++++-- src/app/main/menu.ts | 156 +++++++++++++++++++++ src/app/main/na-auth.ts | 265 ++++++++++++++++++++++++++++++++++++ src/app/main/webservices.ts | 79 +++++++++++ src/app/preload/index.ts | 6 +- tsconfig.json | 11 +- 10 files changed, 649 insertions(+), 16 deletions(-) create mode 100644 src/app/main/menu.ts create mode 100644 src/app/main/na-auth.ts create mode 100644 src/app/main/webservices.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 7b13213..b12032a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,12 @@ "args": [ "dist/app/main/app-entry.cjs" ], - "outputCapture": "std" + "outputCapture": "std", + "env": { + "DEBUG": "*,-express:*,-body-parser:*", + "DEBUG_COLORS": "1", + "FORCE_COLOR": "3" + }, } ] } diff --git a/src/api/na.ts b/src/api/na.ts index bf0f05e..7e848ab 100644 --- a/src/api/na.ts +++ b/src/api/na.ts @@ -137,6 +137,73 @@ export interface NintendoAccountAccessTokenJwtPayload extends JwtPayload { aud: string; } +export enum NintendoAccountScope { + OPENID = 'openid', // Used by NSO, PCTL, nintendo.co.uk + OFFLINE = 'offline', // Used by ec + USER = 'user', // Used by NSO, PCTL, nintendo.co.uk + USER_BIRTHDAY = 'user.birthday', // Used by NSO, PCTL, nintendo.co.uk + USER_MII = 'user.mii', // Used by NSO, nintendo.co.uk + USER_SCREENNAME = 'user.screenName', // Used by NSO + USER_EMAIL = 'user.email', // Used by nintendo.co.uk + USER_LINKS = 'user.links[].id', // Used by nintendo.co.uk + USER_LINKS_NNID = 'user.links.nintendoNetwork.id', // Used by ec + USER_MEMBERSHIP = 'user.membership', // Used by nintendo.co.uk + USER_WISHLIST = 'user.wishlist', // Used by nintendo.co.uk + ESHOP_DEMO = 'eshopDemo', // Used by nintendo.co.uk + ESHOP_DEVICE = 'eshopDevice', // Used by nintendo.co.uk + ESHOP_PRICE = 'eshopPrice', // Used by nintendo.co.uk + MISSIONSTATUS = 'missionStatus', // Used by nintendo.co.uk + MISSIONSTATUS_PROGRESS = 'missionStatus:progress', // Used by nintendo.co.uk + POINTWALLET = 'pointWallet', // Used by nintendo.co.uk + USERNOTIFICATIONMESSAGE_ANYCLIENTS = 'userNotificationMessage:anyClients', // Used by nintendo.co.uk + USERNOTIFICATIONMESSAGE_ANYCLIENTS_WRITE = 'userNotificationMessage:anyClients:write', // Used by nintendo.co.uk + MOONUSER_ADMINISTRATION = 'moonUser:administration', // Used by PCTL + MOONDEVICE_CREATE = 'moonDevice:create', // Used by PCTL + MOONOWNEDDEVICE_ADMINISTRATION = 'moonOwnedDevice:administration', // Used by PCTL + MOONPARENTALCONTROLSETTING = 'moonParentalControlSetting', // Used by PCTL + MOONPARENTALCONTROLSETTING_UPDATE = 'moonParentalControlSetting:update', // Used by PCTL + MOONPARENTALCONTROLSETTINGSTATE = 'moonParentalControlSettingState', // Used by PCTL + MOONPAIRINGSTATE = 'moonPairingState', // Used by PCTL + MOONSMARTDEVICE_ADMINISTRATION = 'moonSmartDevice:administration', // Used by PCTL + MOONDAILYSUMMARY = 'moonDailySummary', // Used by PCTL + MOONMONTHLYSUMMARY = 'moonMonthlySummary', // Used by PCTL +} +export enum NintendoAccountJwtScope { + 'openid' = 0, + 'user' = 8, + 'user.birthday' = 9, + 'user.mii' = 17, + 'user.screenName' = 23, + 'moonUser:administration' = 320, + 'moonDevice:create' = 321, + 'moonOwnedDevice:administration' = 325, + 'moonParentalControlSetting' = 322, + 'moonParentalControlSetting:update' = 323, + 'moonParentalControlSettingState' = 324, + 'moonPairingState' = 326, + 'moonSmartDevice:administration' = 327, + 'moonDailySummary' = 328, + 'moonMonthlySummary' = 329, + + // 10, 12, 70, 81, 198, 288, 289, 291, 292, 356, 357, 376 + // 'user.email' = -1, + // 'user.links[].id' = -1, + // 'user.membership' = -1, + // 'user.wishlist' = -1, + // 'eshopDemo' = -1, + // 'eshopDevice' = -1, + // 'eshopPrice' = -1, + // 'missionStatus' = -1, + // 'missionStatus:progress' = -1, + // 'pointWallet' = -1, + // 'userNotificationMessage:anyClients' = -1, + // 'userNotificationMessage:anyClients:write' = -1, + + // 1, 31 + // 'offline' = -1, + // 'user.links.nintendoNetwork.id' = -1, +} + export interface NintendoAccountUser { emailOptedIn: boolean; language: string; diff --git a/src/app/browser/app.tsx b/src/app/browser/app.tsx index d48fb89..c4cd5c6 100644 --- a/src/app/browser/app.tsx +++ b/src/app/browser/app.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useState } from 'react'; -import { Button, Image, StyleSheet, Text, TextProps, useColorScheme, View } from 'react-native'; +import React, { useCallback } from 'react'; +import { StyleSheet, Text, useColorScheme, View } from 'react-native'; import { NintendoAccountUser } from '../../api/na.js'; import { SavedMoonToken, SavedToken } from '../../util.js'; import ipc from './ipc.js'; diff --git a/src/app/electron.ts b/src/app/electron.ts index fdb66b1..5773056 100644 --- a/src/app/electron.ts +++ b/src/app/electron.ts @@ -3,4 +3,20 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const electron = require('electron'); -export default electron; +export const app = electron.app; +export const BrowserWindow = electron.BrowserWindow; +export const dialog = electron.dialog; +export const ipcMain = electron.ipcMain; +export const Menu = electron.Menu; +export const MenuItem = electron.MenuItem; +export const nativeImage = electron.nativeImage; +export const Notification = electron.Notification; +export const session = electron.session; +export const shell = electron.shell; +export const Tray = electron.Tray; + +export type BrowserWindow = import('electron').BrowserWindow; +export type Menu = import('electron').Menu; +export type MenuItem = import('electron').MenuItem; +export type Notification = import('electron').Notification; +export type Tray = import('electron').Tray; diff --git a/src/app/main/index.ts b/src/app/main/index.ts index 943b708..060f863 100644 --- a/src/app/main/index.ts +++ b/src/app/main/index.ts @@ -1,13 +1,19 @@ -import electron from '../electron.js'; +import { app, BrowserWindow, ipcMain } from '../electron.js'; import * as path from 'path'; -import persist from 'node-persist'; +import createDebug from 'debug'; +import * as persist from 'node-persist'; import { initStorage, paths } from '../../util.js'; +import MenuApp from './menu.js'; +import { EventEmitter } from 'events'; + +import dotenv from 'dotenv'; +import dotenvExpand from 'dotenv-expand'; + +const debug = createDebug('app:main'); const __dirname = path.join(import.meta.url.substr(7), '..'); const bundlepath = path.join(import.meta.url.substr(7), '..', '..', 'bundle'); -const { app, BrowserWindow, ipcMain } = electron; - function createWindow() { // Create the browser window. const mainWindow = new BrowserWindow({ @@ -24,21 +30,49 @@ function createWindow() { } app.whenReady().then(async () => { - createWindow(); + dotenvExpand.expand(dotenv.config({ + path: path.join(paths.data, '.env'), + })); + if (process.env.NXAPI_DATA_PATH) dotenvExpand.expand(dotenv.config({ + path: path.join(process.env.NXAPI_DATA_PATH, '.env'), + })); - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); - }); + if (process.env.DEBUG) createDebug.enable(process.env.DEBUG); - const storage = await initStorage(paths.data); + const storage = await initStorage(process.env.NXAPI_DATA_PATH ?? paths.data); + const store = new Store(storage); ipcMain.handle('nxapi:accounts:list', () => storage.getItem('NintendoAccountIds')); ipcMain.handle('nxapi:nso:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken.' + id)); ipcMain.handle('nxapi:nso:getcachedtoken', (e, token: string) => storage.getItem('NsoToken.' + token)); ipcMain.handle('nxapi:moon:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken-pctl.' + id)); ipcMain.handle('nxapi:moon:getcachedtoken', (e, token: string) => storage.getItem('MoonToken.' + token)); + + const sendToAllWindows = (channel: string, ...args: any[]) => + BrowserWindow.getAllWindows().forEach(w => w.webContents.send(channel, ...args)); + store.on('update-nintendo-accounts', () => sendToAllWindows('nxapi:accounts:shouldrefresh')); + + const menu = new MenuApp(store); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); + + debug('App started'); + + // createWindow(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); + +export class Store extends EventEmitter { + constructor( + public storage: persist.LocalStorage + ) { + super(); + } + + // +} diff --git a/src/app/main/menu.ts b/src/app/main/menu.ts new file mode 100644 index 0000000..1795989 --- /dev/null +++ b/src/app/main/menu.ts @@ -0,0 +1,156 @@ +import { app, dialog, Menu, Tray, nativeImage, MenuItem } from '../electron.js'; +import { addNsoAccount, addPctlAccount } from './na-auth.js'; +import { Store } from './index.js'; +import { getToken, SavedMoonToken, SavedToken } from '../../util.js'; +import { WebService } from '../../api/znc-types.js'; +import openWebService from './webservices.js'; + +export default class MenuApp { + tray: Tray; + + constructor(readonly store: Store) { + const icon = nativeImage.createEmpty(); + + this.tray = new Tray(icon); + + this.tray.setTitle('nxapi'); + this.tray.setToolTip('nxapi'); + + store.on('update-nintendo-accounts', () => this.updateMenu()); + this.updateMenu(); + } + + async updateMenu() { + const menu = new Menu(); + const ids = await this.store.storage.getItem('NintendoAccountIds') as string[] | undefined; + menu.append(new MenuItem({label: 'Nintendo Switch Online', enabled: false})); + + for (const id of ids ?? []) { + const token = await this.store.storage.getItem('NintendoAccountToken.' + id) as string | undefined; + if (!token) continue; + const data = await this.store.storage.getItem('NsoToken.' + token) as SavedToken | undefined; + if (!data) continue; + + const item = new MenuItem({ + label: data.nsoAccount.user.name, + submenu: [ + {label: 'Nintendo Account ID: ' + data.user.id, enabled: false}, + {label: 'NSA ID: ' + data.nsoAccount.user.nsaId, enabled: false}, + {type: 'separator'}, + {label: 'Web services', enabled: false}, + ...await this.getWebServiceItems(token) as any, + ], + }); + } + + menu.append(new MenuItem({label: 'Add account', click: this.addNsoAccount})); + menu.append(new MenuItem({type: 'separator'})); + menu.append(new MenuItem({label: 'Nintendo Switch Parental Controls', enabled: false})); + + for (const id of ids ?? []) { + const token = await this.store.storage.getItem('NintendoAccountToken-pctl.' + id) as string | undefined; + if (!token) continue; + const data = await this.store.storage.getItem('MoonToken.' + token) as SavedMoonToken | undefined; + if (!data) continue; + + const item = new MenuItem({ + label: data.user.nickname, + submenu: [ + {label: 'Nintendo Account ID: ' + data.user.id, enabled: false}, + ], + }); + + menu.append(item); + } + + menu.append(new MenuItem({label: 'Add account', click: this.addPctlAccount})); + + menu.append(new MenuItem({type: 'separator'})); + menu.append(new MenuItem({label: 'Quit', click: () => app.quit()})); + + this.tray.setContextMenu(menu); + } + + addNsoAccount = () => { + addNsoAccount(this.store.storage).catch(err => { + if (err.message === 'Canceled') return; + + dialog.showErrorBox('Error adding account', err.stack || err.message); + }); + }; + + addPctlAccount = () => { + addPctlAccount(this.store.storage).catch(err => { + if (err.message === 'Canceled') return; + + dialog.showErrorBox('Error adding account', err.stack || err.message); + }); + }; + + // Hardcode these temporarily until they are cached + webservices: WebService[] | null = [ + { + id: 4953919198265344, + uri: 'https://web.sd.lp1.acbaa.srv.nintendo.net', + customAttributes: [ + {attrKey: 'verifyMembership', attrValue: 'true'}, + {attrKey: 'deepLinkingEnabled', attrValue: 'true'}, + {attrKey: 'appNavigationBarBgColor', attrValue: '82D7AA'}, + {attrKey: 'appStatusBarBgColor', attrValue: '82D7AA'}, + ], + whiteList: ['*.acbaa.srv.nintendo.net'], + name: 'Animal Crossing: New Horizons', + imageUri: 'https://cdn.znc.srv.nintendo.net/gameWebServices/n5b4648f/n5b4648f/images/euEn/banner.png', + }, + { + id: 5598642853249024, + uri: 'https://app.smashbros.nintendo.net', + customAttributes: [ + {attrKey: 'verifyMembership', attrValue: 'true'}, + {attrKey: 'appNavigationBarBgColor', attrValue: 'A50514'}, + {attrKey: 'appStatusBarBgColor', attrValue: 'A50514'}, + ], + whiteList: ['app.smashbros.nintendo.net'], + name: 'Super Smash Bros. Ultimate', + imageUri: 'https://cdn.znc.srv.nintendo.net/gameWebServices/n3f32691/n3f32691/images/euEn/banner.png', + }, + { + id: 5741031244955648, + uri: 'https://app.splatoon2.nintendo.net/', + customAttributes: [ + {attrKey: 'appNavigationBarBgColor', attrValue: 'E60012'}, + {attrKey: 'appStatusBarBgColor', attrValue: 'E60012'}, + ], + whiteList: ['app.splatoon2.nintendo.net'], + name: 'Splatoon 2', + imageUri: 'https://cdn.znc.srv.nintendo.net/gameWebServices/splatoon2/images/euEn/banner.png', + }, + ]; + + async getWebServices(token: string) { + if (this.webservices) return this.webservices; + + const {nso, data} = await getToken(this.store.storage, token, process.env.ZNC_PROXY_URL); + + const webservices = await nso.getWebServices(); + return this.webservices = webservices.result; + } + + async getWebServiceItems(token: string) { + const webservices = await this.getWebServices(token); + const items = []; + + for (const webservice of webservices) { + items.push(new MenuItem({ + label: webservice.name, + click: async () => { + const {nso, data} = await getToken(this.store.storage, token); + + await openWebService(nso, data, webservice); + }, + })); + } + + return items; + } +} diff --git a/src/app/main/na-auth.ts b/src/app/main/na-auth.ts new file mode 100644 index 0000000..cde7d1a --- /dev/null +++ b/src/app/main/na-auth.ts @@ -0,0 +1,265 @@ +import * as crypto from 'crypto'; +import createDebug from 'debug'; +import * as persist from 'node-persist'; +import fetch from 'node-fetch'; +import { BrowserWindow, nativeImage, Notification, session, shell } from '../electron.js'; +import { getNintendoAccountSessionToken, NintendoAccountSessionToken } from '../../api/na.js'; +import { Jwt } from '../../api/util.js'; +import { ZNCA_CLIENT_ID } from '../../api/znc.js'; +import { getPctlToken, getToken, SavedMoonToken, SavedToken } from '../../util.js'; +import { ZNMA_CLIENT_ID } from '../../api/moon.js'; + +const debug = createDebug('app:main:na-auth'); + +export type NintendoAccountAuthResult = NintendoAccountSessionToken; + +export function getAuthUrl(client_id: string, scope: string | string[]) { + const state = crypto.randomBytes(36).toString('base64url'); + const verifier = crypto.randomBytes(32).toString('base64url'); + const challenge = crypto.createHash('sha256').update(verifier).digest().toString('base64url'); + + const params = { + state, + redirect_uri: 'npf' + client_id + '://auth', + client_id, + scope: typeof scope === 'string' ? scope : scope.join(' '), + response_type: 'session_token_code', + session_token_code_challenge: challenge, + session_token_code_challenge_method: 'S256', + theme: 'login_form', + }; + + const url = 'https://accounts.nintendo.com/connect/1.0.0/authorize?' + + new URLSearchParams(params).toString(); + + return { + url, + state, + verifier, + challenge, + }; +} + +let i = 0; + +export function createAuthWindow() { + const browser_session = session.defaultSession; + + const window = new BrowserWindow({ + width: 400, + height: 600, + resizable: false, + maximizable: false, + fullscreenable: false, + title: 'Nintendo Account', + webPreferences: { + session: browser_session, + scrollBounce: true, + }, + }); + + return window; +} + +export interface NintendoAccountSessionTokenCode { + code: string; + verifier: string; + window?: BrowserWindow; +} + +export function getSessionTokenCode(client_id: string, scope: string | string[], close_window = true) { + return new Promise((rs, rj) => { + const {url: authoriseurl, state, verifier, challenge} = getAuthUrl(client_id, scope); + const window = createAuthWindow(); + + window.webContents.on('will-navigate', (event, url_string) => { + const url = new URL(url_string); + + debug('will navigate', url); + + if (url.protocol === 'npf' + client_id + ':' && url.host === 'auth') { + const authorisedparams = new URLSearchParams(url.hash.substr(1)); + debug('Redirect URL parameters', [...authorisedparams.entries()]); + + const code = authorisedparams.get('session_token_code')!; + const [jwt, sig] = Jwt.decode(code); + + debug('code', code, jwt, sig); + + if (close_window) { + rs({ + code, + verifier, + }); + + window.close(); + } else { + rs({ + code, + verifier, + window, + }); + } + } else if (url.origin === 'https://accounts.nintendo.com') { + // Ok + } else { + event.preventDefault(); + } + }); + + window.on('closed', () => { + rj(new Error('Canceled')); + }); + + window.webContents.on('did-fail-load', e => rj(e)); + + window.webContents.userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1'; + + window.webContents.setWindowOpenHandler(details => { + debug('open', details); + shell.openExternal(details.url); + return {action: 'deny'}; + }); + + debug('Loading Nintendo Account authorisation', { + authoriseurl, + state, + verifier, + challenge, + }); + + window.loadURL(authoriseurl); + }); +} + +const NSO_SCOPE = [ + 'openid', + 'user', + 'user.birthday', + 'user.mii', + 'user.screenName', +]; + +export async function addNsoAccount(storage: persist.LocalStorage) { + const {code, verifier, window} = await getSessionTokenCode(ZNCA_CLIENT_ID, NSO_SCOPE, false); + + window?.setFocusable(false); + window?.blurWebView(); + + try { + const [jwt, sig] = Jwt.decode(code); + + const nsotoken = await storage.getItem('NintendoAccountToken.' + jwt.payload.sub) as string | undefined; + + if (nsotoken) { + const data = await storage.getItem('NsoToken.' + nsotoken) as SavedToken | undefined; + + debug('Already authenticated', data); + + let icon = undefined; + + try { + const response = await fetch(data!.nsoAccount.user.imageUri); + const image = await response.buffer(); + icon = nativeImage.createFromBuffer(image); + } catch (err) {} + + new Notification({ + title: 'Nintendo Switch Online', + body: 'Already signed in as ' + data?.nsoAccount.user.name + ' (' + data?.user.nickname + ')', + icon, + }).show(); + + return getToken(storage, nsotoken, process.env.ZNC_PROXY_URL); + } + + const token = await getNintendoAccountSessionToken(code, verifier, ZNCA_CLIENT_ID); + + debug('session token', token); + + const {nso, data} = await getToken(storage, token.session_token, process.env.ZNC_PROXY_URL); + + const users = new Set(await storage.getItem('NintendoAccountIds') ?? []); + users.add(data.user.id); + await storage.setItem('NintendoAccountIds', [...users]); + + let icon = undefined; + + try { + const response = await fetch(data.nsoAccount.user.imageUri); + const image = await response.buffer(); + icon = nativeImage.createFromBuffer(image); + } catch (err) {} + + new Notification({ + title: 'Nintendo Switch Online', + body: 'Authenticated as ' + data.nsoAccount.user.name + ' (NSO ' + data.user.nickname + ')', + icon, + }).show(); + + return {nso, data}; + } finally { + window?.close(); + } +} + +const MOON_SCOPE = [ + 'openid', + 'user', + 'user.mii', + 'moonUser:administration', + 'moonDevice:create', + 'moonOwnedDevice:administration', + 'moonParentalControlSetting', + 'moonParentalControlSetting:update', + 'moonParentalControlSettingState', + 'moonPairingState', + 'moonSmartDevice:administration', + 'moonDailySummary', + 'moonMonthlySummary', +]; + +export async function addPctlAccount(storage: persist.LocalStorage) { + const {code, verifier, window} = await getSessionTokenCode(ZNMA_CLIENT_ID, MOON_SCOPE, false); + + window?.setFocusable(false); + window?.blurWebView(); + + try { + const [jwt, sig] = Jwt.decode(code); + + const moontoken = await storage.getItem('NintendoAccountToken-pctl.' + jwt.payload.sub) as string | undefined; + + if (moontoken) { + const data = await storage.getItem('MoonToken.' + moontoken) as SavedMoonToken | undefined; + + debug('Already authenticated', data); + + new Notification({ + title: 'Nintendo Switch Parental Controls', + body: 'Already signed in as ' + data?.user.nickname, + }).show(); + + return getPctlToken(storage, moontoken); + } + + const token = await getNintendoAccountSessionToken(code, verifier, ZNMA_CLIENT_ID); + + debug('session token', token); + + const {moon, data} = await getPctlToken(storage, token.session_token); + + const users = new Set(await storage.getItem('NintendoAccountIds') ?? []); + users.add(data.user.id); + await storage.setItem('NintendoAccountIds', [...users]); + + new Notification({ + title: 'Nintendo Switch Parental Controls', + body: 'Authenticated as ' + data.user.nickname, + }).show(); + + return {moon, data}; + } finally { + window?.close(); + } +} diff --git a/src/app/main/webservices.ts b/src/app/main/webservices.ts new file mode 100644 index 0000000..31c926c --- /dev/null +++ b/src/app/main/webservices.ts @@ -0,0 +1,79 @@ +import createDebug from 'debug'; +import { BrowserWindow, session, shell } from '../electron.js'; +import ZncApi from '../../api/znc.js'; +import { SavedToken } from '../../util.js'; +import { WebService } from '../../api/znc-types.js'; + +const debug = createDebug('app:main:webservices'); + +export function createWebServiceWindow(na_id: string, webservice: WebService) { + const browser_session = session.fromPartition('webservices-' + na_id, { + cache: false, + }); + + const window = new BrowserWindow({ + width: 375, + height: 667, + resizable: false, + title: webservice.name, + webPreferences: { + session: browser_session, + scrollBounce: true, + }, + }); + + return window; +} + +const windows = new Map(); + +export default async function openWebService(nso: ZncApi, data: SavedToken, webservice: WebService) { + const windowid = data.user.id + ':' + webservice.id; + + if (windows.has(windowid)) { + const window = windows.get(windowid)!; + + window.focus(); + + return; + } + + const window = createWebServiceWindow(data.user.id, webservice); + + window.on('closed', () => { + windows.delete(windowid); + }); + + window.webContents.userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1'; + + window.webContents.setWindowOpenHandler(details => { + debug('open', details); + shell.openExternal(details.url); + return {action: 'deny'}; + }); + + const webserviceToken = await nso.getWebServiceToken('' + webservice.id); + + const url = new URL(webservice.uri); + url.search = new URLSearchParams({ + lang: data.user.language, + na_country: data.user.country, + na_lang: data.user.language, + }).toString(); + + debug('Loading web service', { + url: url.toString(), + webservice, + webserviceToken, + }); + + window.webContents.openDevTools(); + + window.loadURL(url.toString(), { + extraHeaders: Object.entries({ + 'x-gamewebtoken': webserviceToken.result.accessToken, + 'dnt': '1', + 'X-Requested-With': 'com.nintendo.znca', + }).map(([key, value]) => key + ': ' + value).join('\n'), + }); +} diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts index 0d103ee..7610946 100644 --- a/src/app/preload/index.ts +++ b/src/app/preload/index.ts @@ -1,5 +1,5 @@ import { contextBridge, ipcRenderer } from 'electron'; -import * as path from 'path'; +import * as EventEmitter from 'events'; import { SavedMoonToken, SavedToken } from '../../util.js'; const ipc = { @@ -8,8 +8,12 @@ const ipc = { getSavedNsoToken: (token: string) => ipcRenderer.invoke('nxapi:nso:getcachedtoken', token) as Promise, getNintendoAccountMoonToken: (id: string) => ipcRenderer.invoke('nxapi:moon:gettoken', id) as Promise, getSavedMoonToken: (token: string) => ipcRenderer.invoke('nxapi:moon:getcachedtoken', token) as Promise, + + events: new EventEmitter(), }; export type NxapiElectronIpc = typeof ipc; +ipcRenderer.on('nxapi:accounts:shouldrefresh', () => ipc.events.emit('update-nintendo-accounts')); + contextBridge.exposeInMainWorld('nxapiElectronIpc', ipc); diff --git a/tsconfig.json b/tsconfig.json index 7afa9a2..e0c49b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,13 @@ "declaration": true, "rootDir": "src", "outDir": "dist", - "skipLibCheck": true - } + "skipLibCheck": true, + "allowJs": true + }, + "exclude": [ + "dist", + "bin", + "rollup.config.js", + "data" + ] }