Browse Source

initial commit

master
Yorick van Pelt 2 years ago
parent
commit
bc769925e9
Signed by: yorick GPG Key ID: A36E70F9DC014A15
7 changed files with 607 additions and 0 deletions
  1. +1
    -0
      .gitattributes
  2. +4
    -0
      .gitignore
  3. BIN
      credentials.json
  4. +12
    -0
      fix-readline.js
  5. +274
    -0
      index.js
  6. +299
    -0
      package-lock.json
  7. +17
    -0
      package.json

+ 1
- 0
.gitattributes View File

@ -0,0 +1 @@
credentials.json filter=git-crypt diff=git-crypt

+ 4
- 0
.gitignore View File

@ -0,0 +1,4 @@
*.pdf
node_modules/
payslips/
token.json

BIN
credentials.json View File


+ 12
- 0
fix-readline.js View File

@ -0,0 +1,12 @@
const readline = require('readline');
const {promisify} = require('util');
readline.Interface.prototype.question[promisify.custom] = function(prompt) {
return new Promise(resolve =>
readline.Interface.prototype.question.call(this, prompt, resolve),
);
};
readline.Interface.prototype.questionAsync = promisify(
readline.Interface.prototype.question,
);
module.exports = readline

+ 274
- 0
index.js View File

@ -0,0 +1,274 @@
const readline = require('./fix-readline.js')
const nodemailer = require('nodemailer')
const fs = require('fs');
const {google} = require('googleapis');
const r2 = require('r2')
const qs = require('querystring')
const {promisify} = require('util')
const readFile = promisify(fs.readFile)
const base64url = require('base64url')
const process = require('process')
// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/drive','https://www.googleapis.com/auth/gmail.compose'];
const USER = process.env['USER']
const UserCapitalized = USER.charAt(0).toUpperCase() + USER.slice(1)
const config = {
tokenPath: 'token.json',
fileId: "1CqUglyFNoEWL0lx-e7arqUGOOZEYW__JjcwBmNaXE6E",
template: `${UserCapitalized} ${(new Date()).getFullYear()} Template`,
to: "billing@serokell.io",
destdir: "payslips"
}
async function authAll() {
const content = await readFile('credentials.json')
return authorize(JSON.parse(content))
}
async function authorize(credentials) {
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]);
// Check if we have previously stored a token.
let token
try {
token = await readFile(config.tokenPath)
} catch(e) {
return getAccessToken(oAuth2Client)
}
oAuth2Client.setCredentials(JSON.parse(token));
return oAuth2Client
}
async function ask(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await rl.questionAsync(question)
rl.close()
return answer
}
async function getAccessToken(oAuth2Client) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
const code = await ask('Enter the code from that page here: ')
const token = (await oAuth2Client.getToken(code)).tokens
oAuth2Client.setCredentials(token);
await promisify(fs.writeFile)(config.tokenPath, JSON.stringify(token))
console.log('Token stored to', config.tokenPath);
return oAuth2Client
}
class InvoiceDocument {
constructor(auth, fileId) {
Object.assign(this, {
auth, fileId,
sheets: google.sheets({version: 'v4', auth}),
})
}
async sheetByName(sheetname) {
const {sheets} = (await this.sheets.spreadsheets.get({
spreadsheetId: this.fileId,
includeGridData: false
})).data
const r = sheets.find(({properties: {title}}) => title == sheetname)
return r ? r.properties.sheetId : r
}
async sheetAsPdf(destFile, gid) {
const params = {
format: 'pdf', size: 7, fzr: true,
portrait: true, fitw: false, gridlines: false,
printtitle: false, sheetnames: false, pagenum: "UNDEFINED",
attachment: false, gid, printnotes: false
}
const token = this.auth.credentials.access_token
const dest = fs.createWriteStream(destFile)
// thanks, https://gist.github.com/Spencer-Easton/78f9867a691e549c9c70#gistcomment-2622947
const res = await r2(`https://docs.google.com/spreadsheets/d/${this.fileId}/export?${qs.stringify(params)}`,
{headers: {"Authorization": `Bearer ${token}`}}).response
res.body.pipe(dest)
return new Promise((resolve, reject) => {
res.body.on('end', resolve)
.on('error', reject)
})
}
async copyTo(src, dest) {
const templateId = await this.sheetByName(src)
if (!templateId) throw "template sheet not found"
const res = await this.sheets.spreadsheets.sheets.copyTo({
spreadsheetId: this.fileId,
sheetId: templateId,
resource: {
destinationSpreadsheetId: this.fileId
}
})
const updres = await this.sheets.spreadsheets.batchUpdate({
spreadsheetId: this.fileId,
resource: {
requests: [{
updateSheetProperties: {
fields: "title",
properties: { sheetId: res.data.sheetId, title: dest }
}
}]
}
})
}
async set(sheetname, vals) {
await this.sheets.spreadsheets.values.batchUpdate({
spreadsheetId: this.fileId,
resource: {
valueInputOption: "USER_ENTERED",
data: Object.keys(vals).map(range => ({range: `'${sheetname}'!${range}`, values: [[vals[range]]]}))
}
})
}
async get(sheetname, cell) {
const res = await this.sheets.spreadsheets.values.get({
spreadsheetId: this.fileId,
range: `'${sheetname}'!${cell}`
})
return res.data.values
}
}
async function composeDraft(title, iban, total, pdf, auth) {
const gmail = google.gmail({version: 'v1', auth})
const user = await gmail.users.getProfile({userId: 'me'})
const email = user.data.emailAddress
const transporter = nodemailer.createTransport({
streamTransport: true, newline: 'unix'
})
const body = `Hi!
The total is ${total} to ${iban} (same as always). Thanks!`
// TODO: check if same as always
const msg = {
from: email,
to: config.to,
subject: `payslip - ${title}`,
text: body,
attachments: [{
filename: `payslip - ${title}.pdf`,
path: pdf
}]
}
transporter.sendMailAsync = promisify(transporter.sendMail)
const info = await transporter.sendMailAsync(msg)
const pdfStream = fs.createReadStream(pdf)
const draft = await gmail.users.drafts.create({
userId: 'me',
media: {mimeType: 'message/rfc822', body: info.message}
})
console.log(`Draft created, please review and send: https://mail.google.com/mail/u/${email}/#drafts?compose=${draft.data.message.id}`)
}
const datefmt = date =>
`${date.getFullYear()}-${(""+(date.getMonth()+1)).padStart(2, '0')}-${(""+date.getDate()).padStart(2, '0')}`
const methods = {
async export(sheetname, pdfOut) { // save sheet as pdf
console.log("Exporting", sheetname, "to", pdfOut)
const sheetId = await this.doc.sheetByName(sheetname)
return this.doc.sheetAsPdf(pdfOut, sheetId)
},
async create(sheetname, template=config.template) { // copy sheet from template
console.log("Creating", sheetname, "from", template)
return this.doc.copyTo(template, sheetname)
},
async update(sheetname, startDate, endDate, hours) { // set values in sheet
console.log("Setting data in", sheetname)
hours = +hours
const values = {
regular: Math.min(hours, 80),
overwork: Math.max(0, hours-80),
startDate, endDate
}
console.log(values)
this.doc.set(sheetname, {
D16: values.regular, D17: values.overwork,
B9: values.startDate, D9: values.endDate
})
},
async draft(sheetname) { // export pdf+create gmail draft
const pdfname = `${config.destdir}/${sheetname}.pdf`
try {
await promisify(fs.mkdir)(config.destdir, {recursive: true})
} catch(e) {
if (e.code != 'EEXIST') throw e
}
await this.export(sheetname, pdfname)
const [[paid,,,, total]] = await this.doc.get(sheetname, "B33:F33")
const iban = paid.replace(/^Paid to /, '')
console.log(paid, total)
composeDraft(sheetname, iban, total, pdfname, this.auth)
},
async auto(hours) { // just do everything
const date = new Date()
const day = date.getDate()
let part = 'B'
if (day >= 10 && day < 28) {
part = 'A'
} else if (day < 10) {
// talking about last month
date.setMonth(date.getMonth()-1)
}
const month = date.toLocaleString('en-us', {month: 'short'})
const sheetname = `${UserCapitalized} ${date.getFullYear()} ${month} ${part}`
let startDate, endDate
if (part == 'A') {
date.setDate(1)
startDate = datefmt(date)
date.setDate(15)
endDate = datefmt(date)
} else {
date.setDate(16)
startDate = datefmt(date)
date.setMonth(date.getMonth()+1, 0)
endDate = datefmt(date)
}
const sheetId = await this.doc.sheetByName(sheetname)
if (!sheetId) {
await this.create(sheetname)
} else {
const answer = await ask(`Overwrite '${sheetname}'? [yN]: `)
if (answer.trim().toLowerCase()[0] != 'y') {
console.log("Aborting!")
return null
}
}
await this.update(sheetname, startDate, endDate, hours)
await this.draft(sheetname)
},
async authenticate() { // just auth
}
}
const sig = fn =>
fn.toString().split('\n')[0].replace(/^async /, '').replace(/ \{/, '')
// poor man's CLI
const [,, method, ...args] = process.argv
if (method in methods) {
if (args.length < methods[method].length) {
console.error("not enough arguments for method", sig(methods[method]))
} else {
authAll().then((auth) => {
methods.auth = auth
methods.doc = new InvoiceDocument(auth, config.fileId)
methods[method](...args)
})
}
} else {
if (method && method != 'help') console.log("unknown subcall", method)
console.log("valid methods:")
for(const realMethod in methods) {
console.log(" ", sig(methods[realMethod]))
}
process.exit(1)
}

+ 299
- 0
package-lock.json View File

@ -0,0 +1,299 @@
{
"name": "skl-auto-payslip",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"requires": {
"event-target-shim": "5.0.1"
}
},
"agent-base": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
"requires": {
"es6-promisify": "5.0.0"
}
},
"base64-js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"bignumber.js": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
"integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "2.1.1"
}
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"es6-promise": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz",
"integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "4.2.6"
}
},
"event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"fast-text-encoding": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz",
"integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ=="
},
"gaxios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz",
"integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==",
"requires": {
"abort-controller": "3.0.0",
"extend": "3.0.2",
"https-proxy-agent": "2.2.1",
"node-fetch": "2.6.0"
}
},
"gcp-metadata": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz",
"integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==",
"requires": {
"gaxios": "1.8.4",
"json-bigint": "0.3.0"
}
},
"google-auth-library": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz",
"integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==",
"requires": {
"base64-js": "1.3.0",
"fast-text-encoding": "1.0.0",
"gaxios": "1.8.4",
"gcp-metadata": "1.0.0",
"gtoken": "2.3.3",
"https-proxy-agent": "2.2.1",
"jws": "3.2.2",
"lru-cache": "5.1.1",
"semver": "5.7.0"
}
},
"google-p12-pem": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz",
"integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==",
"requires": {
"node-forge": "0.8.3",
"pify": "4.0.1"
}
},
"googleapis": {
"version": "39.2.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-39.2.0.tgz",
"integrity": "sha512-66X8TG1B33zAt177sG1CoKoYHPP/B66tEpnnSANGCqotMuY5gqSQO8G/0gqHZR2jRgc5CHSSNOJCnpI0SuDxMQ==",
"requires": {
"google-auth-library": "3.1.2",
"googleapis-common": "0.7.2"
}
},
"googleapis-common": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-0.7.2.tgz",
"integrity": "sha512-9DEJIiO4nS7nw0VE1YVkEfXEj8x8MxsuB+yZIpOBULFSN9OIKcUU8UuKgSZFU4lJmRioMfngktrbkMwWJcUhQg==",
"requires": {
"gaxios": "1.8.4",
"google-auth-library": "3.1.2",
"pify": "4.0.1",
"qs": "6.7.0",
"url-template": "2.0.8",
"uuid": "3.3.2"
}
},
"gtoken": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz",
"integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==",
"requires": {
"gaxios": "1.8.4",
"google-p12-pem": "1.0.4",
"jws": "3.2.2",
"mime": "2.4.3",
"pify": "4.0.1"
}
},
"https-proxy-agent": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
"requires": {
"agent-base": "4.2.1",
"debug": "3.2.6"
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"json-bigint": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz",
"integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=",
"requires": {
"bignumber.js": "7.2.1"
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "5.1.2"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "1.4.1",
"safe-buffer": "5.1.2"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"requires": {
"yallist": "3.0.3"
}
},
"mime": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.3.tgz",
"integrity": "sha512-QgrPRJfE+riq5TPZMcHZOtm8c6K/yYrMbKIoRfapfiGLxS8OTeIfRhUGW5LU7MlRa52KOAGCfUNruqLrIBvWZw=="
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"node-forge": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.3.tgz",
"integrity": "sha512-5lv9UKmvTBog+m4AWL8XpZnr3WbNKxYL2M77i903ylY/huJIooSTDHyUWQ/OppFuKQpAGMk6qNtDymSJNRIEIg=="
},
"nodemailer": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.1.1.tgz",
"integrity": "sha512-/x5MRIh56VyuuhLfcz+DL2SlBARpZpgQIf2A4Ao4hMb69MHSgDIMPwYmFwesGT1lkRDZ0eBSoym5+JoIZ3N+cQ=="
},
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"r2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/r2/-/r2-2.0.1.tgz",
"integrity": "sha512-EEmxoxYCe3LHzAUhRIRxdCKERpeRNmlLj6KLUSORqnK6dWl/K5ShmDGZqM2lRZQeqJgF+wyqk0s1M7SWUveNOQ==",
"requires": {
"caseless": "0.12.0",
"node-fetch": "2.6.0",
"typedarray-to-buffer": "3.1.5"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "1.0.0"
}
},
"url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
}

+ 17
- 0
package.json View File

@ -0,0 +1,17 @@
{
"name": "skl-auto-payslip",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"base64url": "^3.0.1",
"googleapis": "^39.2.0",
"nodemailer": "^6.1.1",
"r2": "^2.0.1"
}
}

Loading…
Cancel
Save