mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Add Nintendo Account authentication and web services in Electron
This commit is contained in:
parent
a5838ec47b
commit
2bc251a58b
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
|
|
@ -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"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
//
|
||||
}
|
||||
|
|
|
|||
156
src/app/main/menu.ts
Normal file
156
src/app/main/menu.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
265
src/app/main/na-auth.ts
Normal file
265
src/app/main/na-auth.ts
Normal file
|
|
@ -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<NintendoAccountSessionTokenCode>((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();
|
||||
}
|
||||
}
|
||||
79
src/app/main/webservices.ts
Normal file
79
src/app/main/webservices.ts
Normal file
|
|
@ -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<string, BrowserWindow>();
|
||||
|
||||
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'),
|
||||
});
|
||||
}
|
||||
|
|
@ -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<SavedToken | undefined>,
|
||||
getNintendoAccountMoonToken: (id: string) => ipcRenderer.invoke('nxapi:moon:gettoken', id) as Promise<string | undefined>,
|
||||
getSavedMoonToken: (token: string) => ipcRenderer.invoke('nxapi:moon:getcachedtoken', token) as Promise<SavedMoonToken | undefined>,
|
||||
|
||||
events: new EventEmitter(),
|
||||
};
|
||||
|
||||
export type NxapiElectronIpc = typeof ipc;
|
||||
|
||||
ipcRenderer.on('nxapi:accounts:shouldrefresh', () => ipc.events.emit('update-nintendo-accounts'));
|
||||
|
||||
contextBridge.exposeInMainWorld('nxapiElectronIpc', ipc);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@
|
|||
"declaration": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"bin",
|
||||
"rollup.config.js",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user