mirror of
https://github.com/pockethost/pockethost.git
synced 2025-03-30 15:08:30 +00:00
enh: signup api
This commit is contained in:
parent
b80a3b48d9
commit
a662a9bc7b
@ -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",
|
||||
|
@ -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')
|
167
src/mothership-app/pb_hooks/src/index.pb.js
Normal file
167
src/mothership-app/pb_hooks/src/index.pb.js
Normal 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 */,
|
||||
)
|
@ -1 +0,0 @@
|
||||
export * from './init-mothership'
|
@ -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 {}
|
@ -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)
|
||||
}
|
2084
src/mothership-app/pb_hooks/src/random-words.js
Normal file
2084
src/mothership-app/pb_hooks/src/random-words.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
export type Lib = typeof import('./lib')
|
@ -28,5 +28,5 @@
|
||||
"esm": true
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["src/mothership-app/migrations"]
|
||||
"exclude": ["src/mothership-app"]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user