Added authentication

This commit is contained in:
Ian Wijma 2023-11-04 21:57:17 +11:00
commit fecd3db3bc
7 changed files with 2636 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
node_modules/
bin/

2456
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "spotify-local",
"version": "1.0.0",
"description": "",
"bin": "src/index.js",
"main": "src/index.js",
"scripts": {
"build": "pkg package.json"
},
"pkg": {
"scripts": "src/**/*.js",
"targets": [
"node18-linux-x64",
"node18-macos-x64",
"node18-win-x64"
],
"outputPath": "bin"
},
"author": "Ian Wijma",
"license": "ISC",
"dependencies": {
"@inquirer/prompts": "^3.2.0",
"express": "^4.18.2",
"nanoid": "^3.3.6",
"pkg": "^5.8.1",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/node": "^20.8.10",
"@types/yargs": "^17.0.29"
}
}

View File

@ -0,0 +1,86 @@
const {findAvailablePort} = require("../utilities/findAvailablePort");
const {input} = require("@inquirer/prompts");
const querystring = require("querystring");
const {nanoid} = require("nanoid");
const express = require('express')
const {setAuth, hasAuth, authFile} = require("../utilities/authentication");
exports.command = 'authenticate'
exports.description = 'starts the Spotity authentication process'
exports.builder = function (argv) {
return argv.option('client-id')
}
;
exports.handler = async function (argv) {
if (hasAuth()) {
console.log(`spotify.local is already authenticated, remove "${authFile}" is you want to log out.`);
return;
}
const port = await findAvailablePort();
const authenticateRoute = 'authenticate';
const authenticateUrl = `http://localhost:${port}/${authenticateRoute}`;
const tutorial = [
'Visit https://developer.spotify.com/dashboard and click "Create app"',
'Set the "App name" as "spotify.local"',
'Set the "App description" as "My spotify.local"',
'Set the "Website" as "https://code.tmp.dev/ian/spotify.local"',
`Set the "Redirect URI" as "${authenticateUrl}"`,
'Select under "Which API/SDKs are you planning to use?" the "Web API"',
'Sign your life away by agreeing with the "Developer Terms of Service and Design Guidelines"',
'Click on the "Save" button',
'Copy the "Client ID" from the settings page',
];
tutorial.forEach((string, index) => console.log(`${index+1} ${string}`));
const clientId = argv.clientId ?? await input({ message: `${tutorial.length}. Paste the clientId here` })
const originalState = nanoid();
const redirectUrl = new URL('https://accounts.spotify.com/authorize');
redirectUrl.search = querystring.stringify({
response_type: 'code',
client_id: clientId,
scope: [
'user-library-read',
'user-read-private',
'user-library-modify',
'user-follow-modify',
'user-follow-read',
'playlist-read-private',
'playlist-modify-public',
'playlist-modify-private',
'playlist-read-collaborative',
'user-top-read',
'user-read-recently-played',
'user-read-playback-state',
'user-modify-playback-state',
'user-read-currently-playing',
'user-read-playback-position'
].join(' '),
redirect_uri: authenticateUrl,
state: originalState
});
const app = express();
let server;
app.get(`/${authenticateRoute}`, (req, res) => {
const {state, code} = req.query;
if (originalState === state) {
setAuth(code);
res.send('<script>(() => close())()</script>');
server?.close();
console.log(`${tutorial.length+2}. Spotify.local has been authenticated`);
} else {
res.status(400);
res.send('NO')
}
});
server = app.listen(port, () => console.log(`${tutorial.length+1}. Follow the following URL to authenticate: ${redirectUrl}`));
}

7
src/index.js Normal file
View File

@ -0,0 +1,7 @@
const Yargs = require('yargs');
Yargs(process.argv.splice(2))
.commandDir('commands')
.demandCommand()
.help()
.argv;

View File

@ -0,0 +1,16 @@
const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('fs');
const { homedir } = require('os');
const configDir = `${homedir()}/.spotify-local`
const authenticationFile = `${configDir}/authentication`
const ensure = () => {
if (!existsSync(configDir)) mkdirSync(configDir);
if (!existsSync(authenticationFile)) writeFileSync(authenticationFile, '');
return true;
};
exports.authFile = authenticationFile;
exports.setAuth = (code) => ensure() && writeFileSync(authenticationFile, code);
exports.getAuth = () => ensure() && readFileSync(authenticationFile).toString();
exports.hasAuth = () => ensure() && readFileSync(authenticationFile).toString() !== '';

View File

@ -0,0 +1,36 @@
const net = require('net');
const startPort = 5000;
const endPort = 10000;
const portAvailable = (port) => new Promise((resolve, reject) => {
const server = net.createServer();
server.once('error', function(err) {
if (err.code === 'EADDRINUSE') {
reject([null, false])
}
reject([err])
});
server.once('listening', function() {
server.close();
resolve([null, true])
});
console.log(`Checking port ${port}`);
server.listen(port);
});
/**
* @returns {Promise<number>}
*/
exports.findAvailablePort = async function () {
for (let port = startPort; startPort <= endPort; port++) {
const [error, available] = await portAvailable(port);
if (error) throw error;
if (available) return port;
}
}