.*.sass
.*.scss
.*.svg
+/_config/session.json
/_zet/site.*
/database.logjson
/database.logjson.*
--- /dev/null
+let Problem = class {
+ constructor(title, detail, status) {
+ this.title = title
+ this.detail = detail
+ this.status = status
+ }
+
+ static from(error) {
+ return (
+ error instanceof Problem ?
+ error :
+ new Problem('Bad request', error.message, 400)
+ )
+ }
+}
+
+export default Problem
--- /dev/null
+import assert from 'assert'
+import fsPromises from 'fs/promises'
+import http from 'http'
+import https from 'https'
+import stream_buffers from 'stream-buffers'
+import Problem from './Problem.mjs'
+import XDate from 'xdate'
+
+class Session {
+ constructor(path) {
+ this.path = path
+ this.persistent = {url: 'http://localhost:8080', cookies: {}}
+ }
+
+ async load() {
+ let buffer
+ try {
+ buffer = await fsPromises.readFile(this.path)
+ }
+ catch (error) {
+ if (error.code !== 'ENOENT')
+ throw error
+
+ try {
+ await fsPromises.rename(this.path + '.new', this.path)
+ }
+ catch (error1) {
+ throw error // ENOENT will make more sense to the user
+ }
+
+ buffer = await fsPromises.readFile(this.path)
+ }
+ this.persistent = JSON.parse(buffer.toString('utf-8'))
+ }
+
+ async save() {
+ await fsPromises.writeFile(
+ this.path + '.new',
+ Buffer.from(JSON.stringify(this.persistent, null, 2) + '\n', 'utf-8')
+ )
+ try {
+ await fsPromises.unlink(this.path)
+ }
+ catch (error) {
+ if (error.code !== 'ENOENT')
+ throw error
+ }
+ await fsPromises.rename(this.path + '.new', this.path)
+ }
+
+ async api_call(endpoint, ...args) {
+ let url = new URL(this.persistent.url + endpoint)
+
+ let http_or_https, default_port
+ if (url.protocol === 'https:') {
+ http_or_https = https
+ default_port = 443
+ }
+ else {
+ http_or_https = http
+ default_port = 80
+ }
+
+ let buffer = Buffer.from(
+ JSON.stringify(args) + '\n',
+ 'utf-8'
+ )
+
+ let headers = {
+ 'Content-Type': 'application/json',
+ 'Content-Length': buffer.length
+ }
+
+ let cookies = []
+ let now = XDate.now()
+ for (let i in this.persistent.cookies) {
+ let cookie = this.persistent.cookies[i]
+
+ let expires = cookie.expires
+ if (expires !== undefined && now >= new XDate(expires).getTime())
+ continue
+
+ let path = cookie.path
+ if (path !== undefined) {
+ if (path.slice(-1) != '/')
+ path += '/'
+ if (url.pathname.slice(0, path.length) !== path)
+ continue
+ }
+
+ cookies.push(`${i}=${cookie.value}`)
+ }
+ if (cookies.length)
+ headers.Cookie = cookies.join(', ')
+
+ let response = await new Promise(
+ (resolve, reject) => {
+ let request = http_or_https.request(
+ {
+ hostname: url.hostname,
+ port: url.port.length === 0 ? default_port : parseInt(url.port),
+ path: url.pathname,
+ method: 'POST',
+ headers: headers
+ },
+ response => {resolve(response)}
+ )
+ request.on('error', error => {reject(error)})
+ request.write(buffer)
+ request.end()
+ }
+ )
+
+ let response_cookies = response.headers['set-cookie'] || []
+ for (let i = 0; i < response_cookies.length; ++i) {
+ let fields = response_cookies[i].split(';')
+ assert(fields.length >= 1)
+
+ let j = fields[0].indexOf('=')
+ assert(j >= 0)
+ let name = fields[0].slice(0, j).trim()
+ let value = fields[0].slice(j + 1).trim()
+ if (
+ value.length >= 2 &&
+ value.charAt(0) === 0x22 &&
+ value.charAt(value.length - 1) === 0x22
+ )
+ value = value.slice(1, -1)
+ let attrs = {value}
+
+ for (/*let*/ j = 1; j < fields.length; ++j) {
+ let k = fields[j].indexOf('=')
+ let attr/*, value*/
+ if (k >= 0) {
+ attr = fields[j].slice(0, k).trim()
+ value = fields[j].slice(k + 1).trim()
+ }
+ else {
+ attr = fields[j].trim()
+ if (attr.length === 0)
+ continue
+ value = null
+ }
+ attr = attr.toLowerCase()
+ assert(
+ attr === 'expires' ||
+ attr === 'max-age' ||
+ attr === 'domain' ||
+ attr === 'path' ||
+ attr === 'secure' ||
+ attr === 'httponly' ||
+ attr === 'samesite'
+ ) // cannot be 'value'
+ attrs[attr] = value
+ }
+
+ let max_age = attrs['max-age']
+ if (max_age !== undefined) {
+ let expires = new XDate()
+ expires.addSeconds(parseInt(max_age))
+ attrs['expires'] = expires.toUTCString()
+ delete attrs['max-age']
+ }
+
+ this.persistent.cookies[name] = attrs
+ }
+
+ let write_stream = new stream_buffers.WritableStreamBuffer()
+ let data = new Promise(
+ (resolve, reject) => {
+ write_stream.
+ on('finish', () => {resolve(write_stream.getContents())}).
+ on('error', () => {reject()})
+ }
+ )
+ response.pipe(write_stream)
+
+ let result = JSON.parse((await data).toString('utf-8'))
+ if (response.statusCode < 200 || response.statusCode >= 300)
+ throw new Problem(result.title, result.detail, result.status)
+ return result
+ }
+}
+
+export default Session
--- /dev/null
+#!/usr/bin/env node
+
+import stream_buffers from 'stream-buffers'
+import Problem from './Problem.mjs'
+import Session from './Session.mjs'
+
+if (process.argv.length < 3) {
+ console.log(
+ `usage: ${process.argv[0]} ${process.argv[1]} endpoint [args]`
+ )
+ process.exit(1)
+}
+let endpoint = process.argv[2]
+let args = []
+for (let i = 3; i < process.argv.length; ++i)
+ args.push(JSON.parse(process.argv[i]))
+
+let session = new Session('session.json')
+await session.load()
+
+let result
+try {
+ result = await session.api_call(endpoint, ...args)
+}
+catch (error) {
+ let problem = Problem.from(error)
+ console.error('problem:')
+ console.error(' title:', problem.title)
+ console.error(' detail:', problem.detail)
+ console.error(' status:', problem.status)
+
+ await session.save()
+ process.exit(1)
+}
+process.stdout.write(JSON.stringify(result, null, 2) + '\n')
+
+await session.save()
--- /dev/null
+#!/bin/sh
+./set.mjs /api/globals/set.json <globals.json
--- /dev/null
+#!/usr/bin/env node
+
+import stream_buffers from 'stream-buffers'
+import Problem from './Problem.mjs'
+import Session from './Session.mjs'
+
+if (process.argv.length < 3) {
+ console.log(
+ `usage: ${process.argv[0]} ${process.argv[1]} endpoint [args]`
+ )
+ process.exit(1)
+}
+let endpoint = process.argv[2]
+let args = []
+for (let i = 3; i < process.argv.length; ++i)
+ args.push(JSON.parse(process.argv[i]))
+
+let write_stream = new stream_buffers.WritableStreamBuffer()
+let data = new Promise(
+ (resolve, reject) => {
+ write_stream.
+ on('finish', () => {resolve(write_stream.getContents())}).
+ on('error', () => {reject()})
+ }
+)
+process.stdin.pipe(write_stream)
+args.push(JSON.parse((await data).toString('utf-8')))
+
+let session = new Session('session.json')
+await session.load()
+
+try {
+ await session.api_call(endpoint, ...args)
+}
+catch (error) {
+ let problem = Problem.from(error)
+ console.error('problem:')
+ console.error(' title:', problem.title)
+ console.error(' detail:', problem.detail)
+ console.error(' status:', problem.status)
+
+ await session.save()
+ process.exit(1)
+}
+
+await session.save()
--- /dev/null
+#!/usr/bin/env node
+
+import readline from 'readline'
+import util from 'util'
+import Problem from './Problem.mjs'
+import Session from './Session.mjs'
+
+let session = new Session('session.json')
+try {
+ await session.load()
+}
+catch (error) {
+ if (error.code !== 'ENOENT')
+ throw error
+
+ console.log('warning: session.json not found, using defaults')
+}
+
+let rl = readline.createInterface(
+ {
+ input: process.stdin,
+ output: process.stdout,
+ terminal: false
+ }
+)
+let rl_question = util.promisify(rl.question).bind(rl)
+
+let url = await rl_question(`url: [${session.persistent.url || ''}] `)
+if (url.length)
+ session.persistent.url = url
+else if (session.persistent.url === undefined) {
+ console.log('url is required')
+ process.exit(1)
+}
+
+let username = await rl_question(`username: [${session.persistent.username || ''}] `)
+if (username.length)
+ session.persistent.username = username
+else if (session.persistent.username === undefined) {
+ console.log('username is required')
+ process.exit(1)
+}
+
+let password = await rl_question('password: ')
+if (password.length < 8) {
+ console.log('password is minimum 8 characters')
+ process.exit(1)
+}
+
+rl.close()
+
+try {
+ await session.api_call(
+ '/api/account/sign_in.json',
+ session.persistent.username,
+ password
+ )
+}
+catch (error) {
+ let problem = Problem.from(error)
+ console.error('problem:')
+ console.error(' title:', problem.title)
+ console.error(' detail:', problem.detail)
+ console.error(' status:', problem.status)
+
+ await session.save()
+ process.exit(1)
+}
+
+await session.save()
--- /dev/null
+#!/usr/bin/env node
+
+import Problem from './Problem.mjs'
+import Session from './Session.mjs'
+
+let session = new Session('session.json')
+await session.load()
+
+try {
+ await session.api_call('/api/account/sign_out.json')
+}
+catch (error) {
+ let problem = Problem.from(error)
+ console.error('problem:')
+ console.error(' title:', problem.title)
+ console.error(' detail:', problem.detail)
+ console.error(' status:', problem.status)
+
+ await session.save()
+ process.exit(1)
+}
+
+await session.save()
return async (env, head, body, scripts) => {
let favicons = await env.site.get_min_html('/_favicon/favicons.html')
- let globals = await env.site.get_json('/_config/globals.json')
let session_cookie = await _require('/_lib/session_cookie.jst')
// initialize env.session_key, set cookie in env.response
}
)
env.request.pipe(write_stream)
- let args = JSON.parse((await data).toString())
+ let args = JSON.parse((await data).toString('utf-8'))
console.log('endpoint', env.parsed_url.path, 'args', JSON.stringify(args))
result = await handler(...args)
let transaction = await env.site.database.Transaction()
try {
// initialize env.session_key, set cookie in env.response
- let session = await session_cookie(env, transaction)
+ await session_cookie(env, transaction)
+ if (env.signed_in_as === null)
+ throw new Problem('Unauthorized', 'Please sign in first.', 401)
let account = await (
await (
let transaction = await env.site.database.Transaction()
try {
// initialize env.session_key, set cookie in env.response
- let session = await session_cookie(env, transaction)
+ await session_cookie(env, transaction)
+ if (env.signed_in_as === null)
+ throw new Problem('Unauthorized', 'Please sign in first.', 401)
let account = await (
await (
try {
// initialize env.session_key, set cookie in env.response
await session_cookie(env, transaction)
+ if (env.signed_in_as === null)
+ throw new Problem('Unauthorized', 'Please sign in first.', 401)
let account = await (
await (
--- /dev/null
+let logjson = (await import('@ndcode/logjson')).default
+let XDate = require('xdate')
+
+return async env => {
+ let post_request = await _require('/_lib/post_request.jst')
+ let session_cookie = await _require('/_lib/session_cookie.jst')
+ let Problem = await _require('/_lib/Problem.jst')
+
+ await post_request(
+ // env
+ env,
+ // handler
+ async () => {
+ let transaction = await env.site.database.Transaction()
+ try {
+ // initialize env.session_key, set cookie in env.response
+ await session_cookie(env, transaction)
+ if (env.signed_in_as === null)
+ throw new Problem('Unauthorized', 'Please sign in first.', 401)
+
+ let root = await transaction.get({})
+ let account = await (
+ await root.get('accounts', {})
+ ).get(env.signed_in_as)
+ if (
+ !await logjson.logjson_to_json(
+ await account.get('administrator')
+ )
+ )
+ throw new Problem('Unauthorized', 'Not administrator.', 401)
+
+ globals = await logjson.logjson_to_json(
+ await root.get('globals', {})
+ )
+ await transaction.commit()
+ return globals
+ }
+ catch (error) {
+ transaction.rollback()
+ throw error
+ }
+ }
+ )
+}
--- /dev/null
+let logjson = (await import('@ndcode/logjson')).default
+let XDate = require('xdate')
+
+return async env => {
+ let post_request = await _require('/_lib/post_request.jst')
+ let session_cookie = await _require('/_lib/session_cookie.jst')
+ let Problem = await _require('/_lib/Problem.jst')
+
+ await post_request(
+ // env
+ env,
+ // handler
+ async globals => {
+ // coerce and/or validate
+ globals = {
+ site_url: globals.site_url.slice(0, 1024),
+ site_title: globals.site_title.slice(0, 1024),
+ contact_from: globals.contact_from.slice(0, 1024),
+ contact_to: globals.contact_to.slice(0, 1024),
+ feedback_from: globals.feedback_from.slice(0, 1024),
+ feedback_to: globals.feedback_to.slice(0, 1024),
+ noreply_from: globals.noreply_from.slice(0, 1024),
+ noreply_signature: globals.noreply_signature.slice(0, 1024),
+ copyright: globals.copyright.slice(0, 1024)
+ }
+
+ let transaction = await env.site.database.Transaction()
+ try {
+ // initialize env.session_key, set cookie in env.response
+ await session_cookie(env, transaction)
+ if (env.signed_in_as === null)
+ throw new Problem('Unauthorized', 'Please sign in first.', 401)
+
+ let root = await transaction.get({})
+ let account = await (
+ await root.get('accounts', {})
+ ).get(env.signed_in_as)
+ if (
+ !await logjson.logjson_to_json(
+ await account.get('administrator')
+ )
+ )
+ throw new Problem('Unauthorized', 'Not administrator.', 401)
+
+ root.set('globals', transaction.json_to_logjson(globals))
+ await transaction.commit()
+ }
+ catch (error) {
+ transaction.rollback()
+ throw error
+ }
+ }
+ )
+}