enh: signup api

This commit is contained in:
Ben Allfree 2023-11-04 04:22:36 -07:00
parent b80a3b48d9
commit a662a9bc7b
9 changed files with 2252 additions and 212 deletions

View File

@ -14,8 +14,6 @@
"build-pockethost": "concurrently 'pnpm:build:pockethost:*'",
"build:dashboard": "cd frontends/dashboard && pnpm build",
"build:www": "cd frontends/www && pnpm build",
"build:mothership:migrations": "cd src/mothership-app/migrations && copyfiles '*' ../../../dist/mothership-app/migrations",
"build:mothership:hooks": "esbuild --platform=node --format=cjs --bundle --outfile=./dist/mothership-app/pb_hooks/index.pb.js ./src/mothership-app/pb_hooks/src/index.ts ",
"build:pockethost:docker": "cd src/services/PocketBaseService && docker build -t benallfree/pockethost-instance .",
"push:docker": "docker tag benallfree/pockethost-instance benallfree/pockethost-instance:latest && docker push benallfree/pockethost-instance:latest",
"dev": "concurrently 'pnpm:dev:*'",
@ -23,7 +21,6 @@
"dev:www": "cd frontends/www && pnpm start",
"dev:dashboard": "cd frontends/dashboard && pnpm dev",
"dev:daemon:server": "tsx watch src/server.ts",
"dev:daemon:hooks": "chokidar './src/mothership-app/pb_hooks/**' -c 'pnpm build:mothership:hooks' --initial",
"start": "tsx src/server.ts",
"pm2": "pm2 stop all; pm2 del daemon ; pm2 start \"pnpm start\" --name=daemon -l /home/pockethost/logs/daemon-`date +%s`.log",
"plop": "plop",

View File

@ -1,66 +0,0 @@
import { Assert } from '../lib'
type Lib = typeof import('../lib')
console.log(`update-usage`)
// fires only for "users" and "articles" collections
onRecordAfterUpdateRequest((e) => {
const {
newModel,
_unsafe_assert,
startOfMonth,
endOfMonth,
queryOne,
updateInstance,
getInstance,
updateUser,
} = require(`${__hooks}/lib.js`) as Lib
const assert: Assert = _unsafe_assert
const { record } = e
assert(record, `Expected record here`)
const instanceId = record.getString('instanceId')
console.log(instanceId)
const instance = getInstance(instanceId)
assert(instance)
const uid = instance.getString('uid')
assert(uid)
const now = new Date()
const startIso = startOfMonth(now)
const endIso = endOfMonth(now)
{
const result = queryOne(
'SELECT cast(sum(totalSeconds) as int) as t FROM invocations WHERE instanceId={:instanceId} and startedAt>={:startIso} and startedAt<={:endIso}',
{
instanceId,
startIso,
endIso,
},
{
t: 0,
},
)
const secondsThisMonth = result.t
console.log(`Instance seconds, ${secondsThisMonth}`)
updateInstance(instance, { secondsThisMonth })
}
{
const result = queryOne(
'SELECT cast(sum(totalSeconds) as int) as t FROM invocations WHERE uid={:uid} and startedAt>={:startIso} and startedAt<={:endIso}',
{
uid,
startIso,
endIso,
},
{
t: 0,
},
)
const secondsThisMonth = result.t
console.log(`User seconds, ${secondsThisMonth}`)
updateUser(uid, { secondsThisMonth })
}
}, 'invocations')

View File

@ -0,0 +1,167 @@
onAfterBootstrap((e) => {
$app.dao().db().newQuery(`update instances set status='idle'`).execute()
$app
.dao()
.db()
.newQuery(`update invocations set endedAt=datetime('now') where endedAt=''`)
.execute()
})
routerAdd(
'GET',
'/api/signup',
(c) => {
const random = require(`${__hooks}/random-words.js`)
const instanceName = (() => {
let i = 0
while (true) {
i++
if (i > 100) {
return +new Date()
}
const slug = random.generate(2).join(`-`)
try {
const record = $app
.dao()
.findFirstRecordByData('instances', 'subdomain', slug)
} catch {
return slug
}
}
})()
return c.json(200, { instanceName })
} /* optional middlewares */,
)
/*
// HTTP 200
{
status: 'ok'
}
// HTTP 500
{
"code": 500,
"message": "That user account already exists. Try a password reset.",
"data": {
"email": {
"code": "exists",
"message": "That user account already exists. Try a password reset."
}
}
}
{
"code": 500,
"message": "Instance name was taken, sorry aboout that. Try another.",
"data": {
"instanceName": {
"code": "exists",
"message": "Instance name was taken, sorry aboout that. Try another."
}
}
}
*/
// https://pocketbase.io/docs/js-routing/#sending-request-to-custom-routes-using-the-sdks
routerAdd(
'POST',
'/api/signup',
(c) => {
const parsed = (() => {
const rawBody = readerToString(c.request().body)
try {
const parsed = JSON.parse(rawBody)
return parsed
} catch (e) {
throw new BadRequestError(
`Error parsing payload. You call this JSON? ${rawBody}`,
e,
)
}
})()
const email = parsed.email?.trim()
const password = parsed.password?.trim()
const desiredInstanceName = parsed.instanceName?.trim()
const error = (fieldName, slug, description, extra) =>
new ApiError(500, description, {
[fieldName]: new ValidationError(slug, description),
...extra,
})
if (!email) {
throw error(`email`, 'required', 'Email is required')
}
if (!password) {
throw error(`password`, `required`, 'Password is required')
}
if (!desiredInstanceName) {
throw error(`instanceName`, `required`, `Instance name is required`)
}
const userExists = (() => {
try {
const record = $app.dao().findFirstRecordByData('users', 'email', email)
return true
} catch {
return false
}
})()
if (userExists) {
throw error(
`email`,
`exists`,
`That user account already exists. Try a password reset.`,
)
}
$app.dao().runInTransaction((txDao) => {
const usersCollection = $app.dao().findCollectionByNameOrId('users')
const instanceCollection = $app
.dao()
.findCollectionByNameOrId('instances')
const user = new Record(usersCollection)
try {
const username = $app
.dao()
.suggestUniqueAuthRecordUsername(
'users',
'user' + $security.randomStringWithAlphabet(5, '123456789'),
)
user.set('username', username)
user.set('email', email)
user.setPassword(password)
txDao.saveRecord(user)
} catch (e) {
throw error(`email`, `fail`, `Could not create user: ${e}`)
}
try {
const instance = new Record(instanceCollection)
instance.set('subdomain', desiredInstanceName)
instance.set('uid', user.get('id'))
instance.set('status', 'Idle')
instance.set('version', '~0.19.0')
txDao.saveRecord(instance)
} catch (e) {
if (e.toString().match(/ UNIQUE /)) {
throw error(
`instanceName`,
`exists`,
`Instance name was taken, sorry aboout that. Try another.`,
)
}
throw error(`instanceName`, `fail`, `Could not create instance: ${e}`)
}
$mails.sendRecordVerification($app, user)
})
return c.json(200, { status: 'ok' })
} /* optional middlewares */,
)

View File

@ -1 +0,0 @@
export * from './init-mothership'

View File

@ -1,10 +0,0 @@
onAfterBootstrap((e) => {
$app.dao?.()?.db().newQuery(`update instances set status='idle'`)?.execute()
$app
.dao?.()
?.db()
.newQuery(`update invocations set endedAt=datetime('now') where endedAt=''`)
?.execute()
})
export {}

View File

@ -1,130 +0,0 @@
import { InstanceStatus } from '$shared'
import { assert } from '$util'
import { forEach } from '@s-libs/micro-dash'
export type Assert = typeof assert
export {
assert as _unsafe_assert,
createInstance,
forEach,
updateInstance,
updateUser,
}
export type DynamicModelSchema = { [_: string]: string | number }
export const newModel = <T extends DynamicModelSchema>(schema: T) =>
new DynamicModel(schema) as T
export function endOfMonth(now: Date) {
return new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString()
}
export function startOfMonth(now: Date) {
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString()
}
const dao = () => {
const _dao = $app.dao()
assert(_dao)
return _dao
}
export const queryOne = <T extends DynamicModelSchema>(
sql: string,
bindings: DynamicModelSchema,
defaultResult: T,
): T => {
const result = newModel(defaultResult)
dao().db().newQuery(sql)?.bind(bindings)?.one(result)
return result
}
export type DbId = string
export type InstanceId = DbId
export type UserId = DbId
export type InstanceRecord = {
id: InstanceId
secondsThisMonth: number
}
export const _getRecord = (name: string, id: DbId) => {
const record = dao().findRecordById(name, id)
return record
}
export const getInstance = (instanceId: InstanceId) => {
return _getRecord('instances', instanceId)
}
export const getUser = (userId: UserId) => {
return _getRecord('users', userId)
}
function _updateRecord(record: models.Record, fields: DynamicModelSchema) {
forEach(fields, (v, k) => {
record.set(k, v)
})
dao().saveRecord(record)
}
function _getRecordByIdOrRecord(
recordOrInstanceId: models.Record | DbId,
name: string,
): models.Record {
const record = (() => {
if (typeof recordOrInstanceId === 'string')
return _getRecord(name, recordOrInstanceId)
return recordOrInstanceId
})()
assert(record)
return record
}
function createInstance(userId: UserId, subdomain: string): void {
const collection = dao().findCollectionByNameOrId('instances')
const record = new Record(collection, {
// bulk load the record data during initialization
title: 'Lorem ipsum',
active: true,
})
record.set('subdomain', subdomain)
record.set('uid', userId)
record.set('version', '~0.19.0')
record.set('status', InstanceStatus.Idle)
record.set('secondsThisMonth', 0)
record.set('secrets', {})
record.set('maintenance', false)
dao().saveRecord(record)
}
function updateInstance(
instanceId: InstanceId,
fields: Partial<InstanceRecord>,
): void
function updateInstance(
record: models.Record,
fields: Partial<InstanceRecord>,
): void
function updateInstance(
recordOrInstanceId: models.Record | InstanceId,
fields: Partial<InstanceRecord>,
): void {
const record = _getRecordByIdOrRecord(recordOrInstanceId, 'instances')
_updateRecord(record, fields)
}
function updateUser(userId: InstanceId, fields: Partial<InstanceRecord>): void
function updateUser(
record: models.Record,
fields: Partial<InstanceRecord>,
): void
function updateUser(
recordOrUserId: models.Record | UserId,
fields: Partial<InstanceRecord>,
): void {
const record = _getRecordByIdOrRecord(recordOrUserId, 'users')
_updateRecord(record, fields)
}

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
export type Lib = typeof import('./lib')

View File

@ -28,5 +28,5 @@
"esm": true
},
"include": ["./src"],
"exclude": ["src/mothership-app/migrations"]
"exclude": ["src/mothership-app"]
}