Initial public commit

This commit is contained in:
Ben Allfree 2022-09-18 20:15:57 -07:00
commit 27961edd90
83 changed files with 11468 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
node_modules
.secret

5
.rsyncignore Normal file
View File

@ -0,0 +1,5 @@
.git
node_modules
.svelte-kit
*.log
attic

26
attic/Dockerfile-alpine Normal file
View File

@ -0,0 +1,26 @@
FROM nginx:alpine
RUN apk update
RUN apk add -v build-base
RUN apk add -v go
RUN apk add -v ca-certificates
RUN apk add -v git
COPY ./pocketbase /pocketbase
WORKDIR /pocketbase
RUN go clean
RUN go build
RUN chmod +x pocketbase
RUN mv pocketbase /usr/local/bin/pocketbase
WORKDIR /
ADD ./data /data
ADD ./nginx/conf.d /etc/nginx/conf.d
# Notify Docker that the container wants to expose a port.
EXPOSE 80
COPY ./run.sh /run.sh
RUN chmod +x /run.sh
ENTRYPOINT ./run.sh

37
attic/Dockerfile-fly Normal file
View File

@ -0,0 +1,37 @@
FROM nginx:alpine
# RUN apk add openrc
# RUN apt update
# RUN apt-get install -y unzip
# RUN apt-get install -y nginx
# RUN apt-get install -y systemd
RUN apk add -v build-base
RUN apk add -v go
RUN apk add -v ca-certificates
# COPY ./pockethost-init.d /etc/init.d/pockethost
# RUN chmod +x /etc/init.d/pockethost
# RUN service nginx start
# RUN service pockethost start
COPY ./run.sh /run.sh
RUN chmod +x /run.sh
COPY ./pockethost /pockethost
WORKDIR /pockethost
RUN ls
RUN go clean
RUN go build
RUN chmod +x pockethost
RUN mv pockethost /usr/local/bin/pockethost
WORKDIR /
ADD ./data /data
ADD ./nginx/conf.d /etc/nginx/conf.d
# Notify Docker that the container wants to expose a port.
EXPOSE 80
ENTRYPOINT /run.sh

23
attic/Dockerfile-nginx Normal file
View File

@ -0,0 +1,23 @@
FROM nginx:latest
ARG POCKETBASE_VERSION=0.7.2
RUN apt update
RUN apt install -y unzip systemd systemd-sysv
RUN apt install -y systemd
RUN apt install -y systemd-sysv
# Download Pocketbase and install it for AMD64
ADD https://github.com/pocketbase/pocketbase/releases/download/v${POCKETBASE_VERSION}/pocketbase_${POCKETBASE_VERSION}_linux_amd64.zip /tmp/pocketbase.zip
RUN unzip /tmp/pocketbase.zip -d /usr/local/bin/
RUN chmod +x /usr/local/bin/pocketbase
ADD ./data /data
ADD ./nginx/conf.d /etc/nginx/conf.d
COPY ./pockethost-db.service /etc/systemd/system/pockethost-db.service
RUN systemctl enable pockethost-db.service
# RUN echo "::respawn:/usr/local/bin/pocketbase serve --http '127.0.0.1:8090' --dir /data/pockethost" >> /etc/inittab
# Notify Docker that the container wants to expose a port.
EXPOSE 80

22
attic/Dockerfile-raw Normal file
View File

@ -0,0 +1,22 @@
FROM nginx:alpine
RUN apk update
RUN apk add -v build-base
RUN apk add -v go
RUN apk add -v ca-certificates
RUN apk add -v git
COPY ./pocketbase /pocketbase
WORKDIR /pocketbase
RUN go clean
RUN go build
RUN chmod +x pocketbase
RUN mv pocketbase /usr/local/bin/pocketbase
WORKDIR /
# Notify Docker that the container wants to expose a port.
EXPOSE 80
ENTRYPOINT pocketbase serve --http 127.0.0.1:80

19
attic/Dockerfile-ubuntu Normal file
View File

@ -0,0 +1,19 @@
FROM ubuntu
RUN apt update
RUN apt-get install -y golang-go
RUN apt-get install -y ca-certificates
COPY ./pocketbase /pocketbase
WORKDIR /pocketbase
RUN go clean
RUN go build
RUN chmod +x pocketbase
RUN mv pocketbase /usr/local/bin/pocketbase
WORKDIR /
ADD ./data /data
EXPOSE 80
ENTRYPOINT ["tail", "-f", "/dev/null"]

2
attic/build-server.sh Normal file
View File

@ -0,0 +1,2 @@
apt up
apt upgrade -y

6
attic/build.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
docker container rm -f pockethost
docker image rm -f pockethost
docker build -f $1 -t pockethost .
docker run --name pockethost -p 80:80 pockethost

38
attic/fly.toml Normal file
View File

@ -0,0 +1,38 @@
# fly.toml file generated for pockethost on 2022-09-15T20:05:31-07:00
app = "pockethost"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[env]
[experimental]
allowed_public_ports = []
auto_rollback = true
[[services]]
http_checks = []
internal_port = 8090
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"

View File

@ -0,0 +1,53 @@
server {
listen 80;
server_name db.*;
access_log /data/nginx/access.log main;
error_log /data/nginx/error.log;
location / {
# check http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_read_timeout 180s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8090;
}
}
server {
listen 80;
listen [::]:80;
server_name www.*;
access_log /data/nginx/access.log main;
error_log /data/nginx/error.log;
location / {
root /data/pockethost_html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /data/pockethost_html;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

View File

@ -0,0 +1,16 @@
server {
listen 80;
server_name example.org;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name example.org;
location / {
proxy_pass http://example.org; #for demo purposes
}
}

32
attic/nginx/nginx.conf Normal file
View File

@ -0,0 +1,32 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}

View File

@ -0,0 +1,16 @@
[Unit]
Description=Pockethost DB Service
[Service]
StandardOutput = append:/data/pockethost/logs/errors.log
StandardError = append:/data/pockethost/logs/errors.log
ExecStart=/usr/local/bin/pockethost serve --http '127.0.0.1:8090' --dir /data/pockethost
Restart=always
[Install]
WantedBy=multi-user.target
[Unit]
Description = pocketbase

23
attic/pockethost-init.d Normal file
View File

@ -0,0 +1,23 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: pockethost
# Short-Description: starts the pockethost db server
# Description: starts the pockethost db server
### END INIT INFO
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
NAME=pockethost
DESC=pockethost
case "$1" in
start)
echo "Starting $DESC" "$NAME"
pockethost serve --http '127.0.0.1:8090' --dir /data/pockethost &
;;
stop)
echo "Stopping $DESC" "$NAME"
esac

View File

@ -0,0 +1,11 @@
#!/sbin/openrc-run
description="Pockethost server"
extra_commands=""
extra_started_commands=""
start_pre() {
einfo "Starting pockethost"
/usr/local/bin/pocketbase serve --http '127.0.0.1:8090' --dir /data/pockethost
}

1
attic/pockethost/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
pockethost

83
attic/pockethost/go.mod Normal file
View File

@ -0,0 +1,83 @@
module pockethost
go 1.19
require (
github.com/AlecAivazis/survey/v2 v2.3.5 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go v1.44.85 // indirect
github.com/aws/aws-sdk-go-v2 v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect
github.com/aws/smithy-go v1.12.1 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.3.4 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
github.com/ganigeorgiev/fexpr v0.1.1 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/gax-go/v2 v2.5.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/pocketbase/dbx v1.6.0 // indirect
github.com/pocketbase/pocketbase v0.7.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
go.opencensus.io v0.23.0 // indirect
gocloud.dev v0.26.0 // indirect
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/api v0.94.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.36.3 // indirect
modernc.org/ccgo/v3 v3.16.9 // indirect
modernc.org/libc v1.17.0 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.2.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.18.1 // indirect
modernc.org/strutil v1.1.2 // indirect
modernc.org/token v1.0.0 // indirect
)

1109
attic/pockethost/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
package main
import (
"log"
"github.com/pocketbase/pocketbase"
)
func main() {
app := pocketbase.New()
if err := app.Start(); err != nil {
log.Fatal(err)
}
}

4
attic/run.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
nginx
pocketbase serve

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "pockethost",
"version": "0.0.1",
"main": "index.js",
"author": "Ben Allfree <ben@benallfree.com>",
"license": "MIT",
"private": true,
"scripts": {
"sync": "rsync -avv --exclude-from=.rsyncignore ./* pockethost@pockethost.io:~/pockethost --delete",
"watch": "chokidar './**/*' -i .git -i '**/node_modules' -i '.svelte-kit' -c 'yarn sync' --initial"
},
"workspaces": [
"packages/fcs-client",
"packages/worker",
"packages/admin",
"packages/*"
],
"prettier": {
"semi": false,
"singleQuote": true,
"plugins": [
"./node_modules/prettier-plugin-organize-imports",
"./node_modules/prettier-plugin-svelte"
]
},
"devDependencies": {
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.1.1",
"prettier-plugin-svelte": "^2.7.0",
"typescript": "^4.8.3"
}
}

View File

@ -0,0 +1,11 @@
{
"name": "@pockethost/common",
"version": "0.0.1",
"main": "src/index.ts",
"license": "MIT",
"dependencies": {
"nanoid": "^4.0.0",
"pocketbase": "^0.7.0",
"ts-brand": "^0.0.2"
}
}

View File

@ -0,0 +1,29 @@
import { pocketbase } from './pocketbase'
import { Any_Record_Out } from './schema'
export const createRealtimeSubscriptionManager = () => {
const subscriptions: { [_: string]: number } = {}
const subscribe = <TRec extends Any_Record_Out>(
slug: string,
cb: (rec: TRec) => void
) => {
if (subscriptions[slug]) {
subscriptions[slug]++
} else {
subscriptions[slug] = 1
pocketbase.realtime.subscribe(slug, (e) => {
console.log(`Realtime update`, { e })
cb(e.record as unknown as TRec)
})
}
return () => {
subscriptions[slug]--
if (subscriptions[slug] === 0) {
pocketbase.realtime.unsubscribe(slug)
}
}
}
return subscribe
}

View File

@ -0,0 +1,8 @@
export function assertExists<TType>(
v: unknown,
message = `Value does not exist`
): asserts v is NonNullable<TType> {
if (typeof v === 'undefined') {
throw new Error(message)
}
}

View File

@ -0,0 +1 @@
export const isBrowser = () => typeof window !== 'undefined'

View File

@ -0,0 +1,87 @@
import pocketbaseEs, { BaseAuthStore } from 'pocketbase'
import type { Unsubscriber } from 'svelte/store'
import { identity } from 'ts-brand'
import { createRealtimeSubscriptionManager } from './RealtimeSubscriptionManager'
import type {
InstanceId,
Instance_In,
Instance_Out,
Instance_Out_ByIdCollection,
} from './schema'
const client = new pocketbaseEs('https://pockethost-central.pockethost.io')
const { authStore } = client
const { onChange } = authStore
export const user = () => authStore.model
export const isLoggedIn = () => authStore.isValid
export const onAuthChange = (cb: (user: BaseAuthStore) => Unsubscriber) =>
onChange(() => cb(authStore))
export const logOut = () => authStore.clear()
export const createUser = (email: string, password: string) =>
client.users.create({
email,
password,
passwordConfirm: password,
})
export const authViaEmail = (email: string, password: string) =>
client.users.authViaEmail(email, password)
export const pocketbase = client
export const createInstance = (payload: Instance_In): Promise<Instance_Out> => {
return pocketbase.records
.create('instances', payload)
.then((r) => r as unknown as Instance_Out)
}
export const getInstanceById = (
id: InstanceId
): Promise<Instance_Out | undefined> =>
pocketbase.records
.getOne('instances', id)
.then((r) => r as unknown as Instance_Out)
const subscribe = createRealtimeSubscriptionManager()
export const watchInstanceById = (
id: InstanceId,
cb: (rec: Instance_Out) => void
): Unsubscriber => {
const slug = `instances/${id}`
getInstanceById(id).then((v) => {
if (!v) return
console.log(`Initial record`, { v })
cb(v)
})
return subscribe(slug, cb)
}
export const getAllInstancesById = async () =>
(
await client.records.getFullList('instances').catch((e) => {
console.error(`getAllInstancesById failed with ${e}`)
throw e
})
).reduce((c, v) => {
const _v = identity<Instance_Out>(v)
c[_v.id] = _v
return c
}, {} as Instance_Out_ByIdCollection)
export const setInstance = (instanceId: InstanceId, fields: Instance_In) => {
console.log(`${instanceId} setting fields`, { fields })
return client.records.update('instances', instanceId, fields).catch((e) => {
console.error(`setInstance failed for ${instanceId} with ${e}`, {
fields,
})
throw e
})
}

View File

@ -0,0 +1,69 @@
import { AnyBrand, Brand, identity } from 'ts-brand'
export type UserId = Brand<string, 'UserId'>
export type InstanceId = Brand<string, 'InstanceId'>
export type InternalInstanceId = Brand<string, 'InternalInstanceId'>
export type Subdomain = Brand<string, 'Subdomain'>
export type Port = Brand<number, 'Port'>
export type IsoDate = Brand<string, 'IsoDate'>
export type ProcessId = Brand<number, 'ProcessId'>
export type Username = Brand<string, 'username'>
export type Password = Brand<string, 'password'>
export const pocketNow = () => identity<IsoDate>(new Date().toISOString())
export enum InstanceStatuses {
Unknown = '',
Provisioning = 'provisioning',
Port = 'obtaining port',
Cert = 'creating SSL cert',
Starting = 'starting',
Started = 'started',
Failed = 'failed',
}
export type Instance_In = {
uid?: UserId
subdomain?: Subdomain
status?: InstanceStatuses
}
export type PocketbaseRecord<TIdType extends AnyBrand> = {
id: TIdType
created: IsoDate
updated: IsoDate
}
export type Instance_Out = PocketbaseRecord<InstanceId> & {
uid: UserId
subdomain: Subdomain
status: InstanceStatuses
}
export type Instance_Internal_In = {
instanceId?: InstanceId
port?: Port
certCreatedAt?: IsoDate
nginxCreatedAt?: IsoDate
pid?: ProcessId
launchedAt?: IsoDate
}
export type Instance_Internal_Out = PocketbaseRecord<InternalInstanceId> & {
instanceId: InstanceId
port: Port
certCreatedAt: IsoDate
nginxCreatedAt: IsoDate
pid: ProcessId
launchedAt: IsoDate
}
export type Any_Record_Out = Instance_Out | Instance_Internal_Out
export type Instance_Out_ByIdCollection = {
[_: InstanceId]: Instance_Out
}
export type Instance_Internal_Out_ByIdCollection = {
[_: InstanceId]: Instance_Internal_Out
}

2
packages/daemon/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.parcel-cache
dist

View File

@ -0,0 +1,36 @@
{
"name": "@pockethost/daemon",
"version": "0.0.1",
"license": "MIT",
"scripts": {
"build": "parcel",
"serve": "node dist/server.js"
},
"targets": {
"server": {
"engines": {
"node": ">=18"
},
"source": "src/server.ts",
"includeNodeModules": [
"@pockethost/common",
"get-port",
"pocketbase",
"@s-libs/micro-dash"
]
}
},
"dependencies": {
"@pockethost/common": "0.0.1",
"@s-libs/micro-dash": "^14.1.0",
"bottleneck": "^2.19.5",
"event-source-polyfill": "^1.0.31",
"get-port": "^6.1.2",
"pocketbase": "^0.7.0",
"ts-brand": "^0.0.2"
},
"devDependencies": {
"parcel": "^2.7.0",
"ts-node": "^10.9.1"
}
}

View File

@ -0,0 +1,10 @@
#!/bin/bash
PORT=8090
SUBDOMAIN=db
sudo certbot certonly --agree-tos --nginx --email pockethost@benallfree.com -d $SUBDOMAIN.pockethost.io
NGINX_CONFIG=envsubst < nginx-template.conf
echo $NGINX_CONFIG

View File

@ -0,0 +1 @@
sudo certbot certonly --agree-tos --nginx --email pockethost@benallfree.com -d $SUBDOMAIN.pockethost.io

View File

@ -0,0 +1 @@
pocketbase serve --dir ~/pocketbase-microservice/data/$SUBDOMAIN/pb_data --http 127.0.0.1:$PORT &

View File

@ -0,0 +1,28 @@
server {
listen 443 ssl;
server_name $SUBDOMAIN.pockethost.io;
ssl_certificate /etc/letsencrypt/live/$SUBDOMAIN.pockethost.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$SUBDOMAIN.pockethost.io/privkey.pem;
access_log /home/pockethost/pockethost-microservice/data/$SUBDOMAIN/logs/access.log;
error_log /home/pockethost/pockethost-microservice/data/$SUBDOMAIN/logs/error.log;
location / {
proxy_read_timeout 180s;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:$PORT;
}
}

View File

@ -0,0 +1,78 @@
import { pocketbase } from '@pockethost/common/src/pocketbase'
import {
InstanceId,
Instance_Internal_In,
Instance_Internal_Out,
Instance_Internal_Out_ByIdCollection,
InternalInstanceId,
Password,
Port,
Username,
} from '@pockethost/common/src/schema'
import Bottleneck from 'bottleneck'
import { identity } from 'ts-brand'
const limiter = new Bottleneck({ maxConcurrent: 1 })
const client = pocketbase
export const adminAuthViaEmail = (username: Username, password: Password) =>
client.admins.authViaEmail(username, password)
export const getAllInternalInstancesByInstanceId = async () =>
(
await limiter
.schedule(() => client.records.getFullList('instances_internal'))
.catch((e) => {
console.error(`getAllInternalInstancesById failed with ${e}`)
throw e
})
).reduce((c, v) => {
const _v = identity<Instance_Internal_Out>(v)
c[identity<InstanceId>(_v.instanceId)] = _v
return c
}, {} as Instance_Internal_Out_ByIdCollection)
export const setInternalInstancePort = (
instanceId: InternalInstanceId,
port: Port
) =>
limiter
.schedule(() =>
client.records.update('instances_internal', instanceId, { port })
)
.catch((e) => {
console.error(
`setInternalInstancePort failed for ${instanceId} port ${port} with ${e}`
)
throw e
})
export const setInternalInstance = (
instanceId: InternalInstanceId,
fields: Instance_Internal_In
) => {
console.log(`${instanceId} setting fields`, { fields })
return limiter
.schedule(() =>
client.records.update('instances_internal', instanceId, fields)
)
.catch((e) => {
console.error(`setInternalInstance failed for ${instanceId} with ${e}`, {
fields,
})
throw e
})
}
export const linkInternalInstance = async (instanceId: InstanceId) => {
const _in: Instance_Internal_In = {
instanceId,
}
return (await limiter
.schedule(() => client.records.create('instances_internal', _in))
.catch((e) => {
console.error(`linkInternalInstance failed for ${instanceId} with ${e}`)
throw e
})) as unknown as Instance_Internal_Out
}

View File

@ -0,0 +1,35 @@
import { Port, Subdomain } from '@pockethost/common/src/schema'
export const NGINX_TEMPLATE = (subdomain: Subdomain, port: Port) => `
### BEGIN ${subdomain}:${port} ###
server {
listen 443 ssl;
server_name ${subdomain}.pockethost.io;
ssl_certificate /etc/letsencrypt/live/${subdomain}.pockethost.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${subdomain}.pockethost.io/privkey.pem;
access_log /home/pockethost/data/${subdomain}/logs/access.log;
error_log /home/pockethost/data/${subdomain}/logs/error.log;
location / {
proxy_read_timeout 180s;
# WebSocket support
proxy_buffering off; # For realtime
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:${port};
}
}
### END ${subdomain}:${port} ###
`

View File

@ -0,0 +1,10 @@
import { ProcessId } from '@pockethost/common/src/schema'
export function pidIsRunning(pid: ProcessId) {
try {
process.kill(pid, 0)
return true
} catch (e) {
return false
}
}

View File

@ -0,0 +1,240 @@
import { assertExists } from '@pockethost/common/src/assert'
import {
getAllInstancesById,
setInstance,
} from '@pockethost/common/src/pocketbase'
import {
InstanceId,
InstanceStatuses,
Password,
pocketNow,
Port,
ProcessId,
Subdomain,
Username,
} from '@pockethost/common/src/schema'
import { map } from '@s-libs/micro-dash'
import Bottleneck from 'bottleneck'
import { exec, spawn } from 'child_process'
import { mkdir, writeFile } from 'fs'
import getPort from 'get-port'
import { identity } from 'ts-brand'
import util from 'util'
import {
adminAuthViaEmail,
getAllInternalInstancesByInstanceId,
linkInternalInstance,
setInternalInstance,
setInternalInstancePort,
} from './internal'
import { NGINX_TEMPLATE } from './nginx-template'
import { pidIsRunning } from './pidIsRunning'
const pMkdir = util.promisify(mkdir)
const pWriteFile = util.promisify(writeFile)
const pExec = util.promisify(exec)
const singleLimiter = new Bottleneck({ maxConcurrent: 1 })
const HOME_DIR = `/home/pockethost`
const DATA_ROOT = `${HOME_DIR}/data`
spawn(`pocketbase`, [
`serve`,
`--dir`,
`${DATA_ROOT}/pockethost-central/pb_data`,
`--http`,
`127.0.0.1:8090`,
])
;(async () => {
await adminAuthViaEmail(
identity<Username>(`ben@pockethost.io`),
identity<Password>('c6j3ARgcUvut')
)
console.log(`logged in as admin`)
// Subscribe to changes in any record from the "demo" collection
// client.realtime.subscribe('instances', function (e) {
// console.log(`record changed`, e.record)
// })
// alternatively you can also fetch all records at once via getFullList:
const _check = async () => {
console.log(`Fetching current state`)
const [allInstances, allInternalInstances] = await Promise.all([
getAllInstancesById(),
getAllInternalInstancesByInstanceId(),
])
let needsNginx = false
const nginxConfigs: string[] = [
NGINX_TEMPLATE(
identity<Subdomain>(`pockethost-central`),
identity<Port>(8090)
),
]
const provision = (() => {
const instances: InstanceId[] = []
const add = (instanceId: InstanceId, status: InstanceStatuses) => {
if (instances.indexOf(instanceId) < 0) {
instances.push(instanceId) // Track this instance
}
console.log(`${instanceId} status is ${status}`)
return setInstance(instanceId, {
status,
})
}
const finish = () => {
const p = Promise.all(
instances.map((instanceId) =>
setInstance(instanceId, { status: InstanceStatuses.Started })
)
)
instances.length = 0
return p
}
return { add, finish }
})()
await Promise.all(
map(allInstances, async (instance, instanceId) => {
const ROOT_DIR = `${DATA_ROOT}/${instance.subdomain}`
const LOG_DIR = `${ROOT_DIR}/logs`
const DATA_DIR = `${ROOT_DIR}/pb_data`
console.log(`Examining instance ${instanceId}`)
if (!allInternalInstances[instanceId]) {
console.log(`${instanceId} linking internal`)
await provision.add(instanceId, InstanceStatuses.Provisioning)
allInternalInstances[instanceId] = await linkInternalInstance(
instanceId
).catch((e) => {
console.error(`${instanceId} error linking internal`)
throw e
})
console.log(`${instanceId} done linking internal`)
}
const internalInstance = allInternalInstances[instanceId]
assertExists(internalInstance, `Expected instance here`)
if (!internalInstance.port) {
const exclude = map(allInternalInstances, (i) => i.port).filter(
(v) => !!v
)
console.log(`${instanceId} needs port`, exclude)
await provision.add(instanceId, InstanceStatuses.Port)
const newPort = identity<Port>(
await getPort({
port: 8090,
exclude,
}).catch((e) => {
console.error(`Failed to get port`)
throw e
})
)
await setInternalInstancePort(internalInstance.id, newPort)
internalInstance.port = newPort
console.log(`${instanceId} port ${newPort}`)
needsNginx = true
}
if (!internalInstance.certCreatedAt) {
console.log(`${instanceId} needs cert`)
await provision.add(instanceId, InstanceStatuses.Cert)
const CERTBOT_CMD = `certbot certonly --keep --agree-tos --nginx --email pockethost@benallfree.com -d ${instance.subdomain}.pockethost.io`
await singleLimiter
.schedule(() => pExec(CERTBOT_CMD))
.catch((e) => {
console.error(`${instanceId} certbot error: ${e}`)
throw e
})
const certCreatedAt = pocketNow()
await setInternalInstance(internalInstance.id, {
certCreatedAt,
})
internalInstance.certCreatedAt = certCreatedAt
console.log(`${instanceId} cert created at ${certCreatedAt}`)
needsNginx = true
}
await Promise.all([
pMkdir(LOG_DIR, { recursive: true }),
pMkdir(DATA_DIR, { recursive: true }),
])
needsNginx = needsNginx || !internalInstance.nginxCreatedAt
nginxConfigs.push(
NGINX_TEMPLATE(instance.subdomain, internalInstance.port)
)
if (!internalInstance.pid || !pidIsRunning(internalInstance.pid)) {
console.log(`${instanceId} PocketHost instance is not running`)
await provision.add(instanceId, InstanceStatuses.Starting)
const child = spawn(`pocketbase`, [
`serve`,
`--dir`,
DATA_DIR,
`--http`,
`127.0.0.1:${internalInstance.port}`,
])
const { pid } = child
assertExists<ProcessId>(pid, `Expected PID for ${instanceId}`)
const launchedAt = pocketNow()
setInternalInstance(internalInstance.id, {
pid,
launchedAt,
})
internalInstance.pid = pid
internalInstance.launchedAt = launchedAt
console.log(
`${instanceId} PocketHost instance is running on PID ${pid}`
)
}
})
)
if (needsNginx) {
console.log(`NGINX needs a rebuild`)
const configs = nginxConfigs.join(`\n`)
console.log(`NGINX config`)
const NGINX_CONF = `/etc/nginx/sites-enabled/_instances`
await pWriteFile(NGINX_CONF, configs).catch((e) => {
console.error(`Error writing nginx conf: ${e}`)
throw e
})
await pExec(`systemctl reload nginx`)
await Promise.all(
map(allInternalInstances, async (internalInstance) => {
const nginxCreatedAt = pocketNow()
setInternalInstance(internalInstance.id, { nginxCreatedAt })
})
)
}
await provision.finish()
console.log(`Finished with everything, checking again soon`)
}
const _recheck = () => {
setTimeout(
() =>
_check()
.catch((e) => {
console.error(`Check failed: ${e}`)
})
.finally(() => {
setImmediate(_recheck)
}),
1000
)
}
_recheck()
})().catch((e) => {
console.error(`Fatal error`, e)
})

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"module": "ESNext",
"moduleResolution": "node",
"noUncheckedIndexedAccess": true,
"strictNullChecks": true
},
"ts-node": {
"esm": true
},
"include": ["./src"]
}

18
packages/daemon/yarn.lock Normal file
View File

@ -0,0 +1,18 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
event-source-polyfill@^1.0.31:
version "1.0.31"
resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz#45fb0a6fc1375b2ba597361ba4287ffec5bf2e0c"
integrity sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==
get-port@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a"
integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==
pocketbase@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/pocketbase/-/pocketbase-0.7.0.tgz#bfce0225e03321cfa816f589a6dee60ec8b2f726"
integrity sha512-55le5D1dfthouWzxTIkL3eMgEsQqZsdT9O5HWFkCmbw889EKZsz0kf+2kkWgPvZqeVVXfOhMpJNHVOK5L2fJ3A==

1
packages/pocketbase/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
pocketbase

View File

@ -0,0 +1,83 @@
module pocketbase
go 1.19
require (
github.com/AlecAivazis/survey/v2 v2.3.5 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go v1.44.85 // indirect
github.com/aws/aws-sdk-go-v2 v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect
github.com/aws/smithy-go v1.12.1 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.3.4 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
github.com/ganigeorgiev/fexpr v0.1.1 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/gax-go/v2 v2.5.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/pocketbase/dbx v1.6.0 // indirect
github.com/pocketbase/pocketbase v0.7.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
go.opencensus.io v0.23.0 // indirect
gocloud.dev v0.26.0 // indirect
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/api v0.94.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.36.3 // indirect
modernc.org/ccgo/v3 v3.16.9 // indirect
modernc.org/libc v1.17.0 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.2.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.18.1 // indirect
modernc.org/strutil v1.1.2 // indirect
modernc.org/token v1.0.0 // indirect
)

1109
packages/pocketbase/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
package main
import (
"log"
"github.com/pocketbase/pocketbase"
)
func main() {
app := pocketbase.New()
if err := app.Start(); err != nil {
log.Fatal(err)
}
}

9
packages/pockethost.io/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
dist-server

View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -0,0 +1,19 @@
{
"useTabs": true,
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"printWidth": 100,
"pluginSearchDirs": [
".",
"../.."
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View File

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View File

@ -0,0 +1,39 @@
{
"name": "@pockethost/app",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check .",
"format": "prettier --write .",
"serve": "node dist-server/index.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"svelte": "^3.44.0",
"svelte-check": "^2.7.1",
"svelte-preprocess": "^4.10.6",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^3.1.0"
},
"type": "module",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@pockethost/common": "0.0.1",
"@s-libs/micro-dash": "12",
"@sveltejs/adapter-node": "^1.0.0-next.92",
"pocketbase": "^0.7.0",
"random-word-slugs": "^0.1.6",
"sass": "^1.54.9",
"svelte-fa": "^3.0.3",
"svelte-highlight": "^6.2.1",
"ts-brand": "^0.0.2"
}
}

13
packages/pockethost.io/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface PageError {}
// interface Platform {}
}
declare module '@fortawesome/pro-solid-svg-icons/index.es' {
export * from '@fortawesome/pro-solid-svg-icons'
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" href="%sveltekit.assets%/global.css" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,10 @@
<script lang="ts">
import { isLoggedIn, user } from '@pockethost/common/src/pocketbase'
import Button from './Button/Button.svelte'
if (typeof window !== 'undefined' && isLoggedIn()) {
window.location.href = '/dashboard'
}
</script>
<slot />

View File

@ -0,0 +1,50 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { ButtonStyles } from './types'
export let style: ButtonStyles = ButtonStyles.Wide
export let disabled = false
export let href: string = '#'
export let click: () => void = () => {
goto(href)
}
</script>
<button class={style} {disabled} on:click={click}> <slot /></button>
<style type="text/scss">
button,
.button {
min-width: 100px;
width: 100px;
white-space: nowrap;
border-radius: 5px;
color: white;
background: rgb(10, 10, 120);
display: inline-block;
margin-left: auto;
margin-right: auto;
padding: 10px;
font-size: 30px;
&:hover {
cursor: pointer;
text-decoration: none;
}
&:disabled {
background: rgb(64, 64, 107);
color: lightgray;
}
&.wide {
width: 100%;
}
&.micro {
width: initial;
padding: 2px;
padding-left: 5px;
padding-right: 5px;
min-width: initial;
max-width: initial;
font-size: 10px;
}
}
</style>

View File

@ -0,0 +1,5 @@
export enum ButtonStyles {
Micro = 'micro',
Normal = 'normal',
Wide = 'wide',
}

View File

@ -0,0 +1,30 @@
<script lang="ts">
import { CaptionSize } from "./types"
export let size:CaptionSize=CaptionSize.Normal
</script>
<div class="caption {size}"><slot /></div>
<style lang="scss">
.caption {
margin-top: 20px;
margin-bottom: 20px;
&.small {
font-size: 15px;
}
&.normal {
font-size: 25px;
}
&.large {
font-size: 45px;
}
&.hero {
font-size: 65px;
}
}
</style>

View File

@ -0,0 +1,6 @@
export enum CaptionSize {
Small='small',
Normal='normal',
Large='large',
Hero='hero'
}

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { onMount, tick, createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let text: string
let textarea: HTMLTextAreaElement
async function copy() {
textarea.select()
document.execCommand('Copy')
await tick()
textarea.blur()
dispatch('copy')
}
</script>
<slot {copy} />
<textarea bind:this={textarea} value={text} />
<style>
textarea {
left: 0;
bottom: 0;
margin: 0;
padding: 0;
opacity: 0;
width: 1px;
height: 1px;
border: none;
display: block;
position: absolute;
}
</style>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { Highlight } from 'svelte-highlight'
import { typescript } from 'svelte-highlight/languages'
import Clipboard from './Clipboard.svelte'
import 'svelte-highlight/styles/github.css'
import Button from './Button/Button.svelte'
import { ButtonStyles } from './Button/types'
import CopyButton from './CopyButton.svelte'
export let code: string
const handleCopy = () => {
console.log('copied')
}
</script>
<div class="copy-container">
<Highlight language={typescript} {code} />
<div class="copy-button">
<CopyButton {code} copy={handleCopy} />
</div>
</div>
<style type="text/scss">
.copy-container {
position: relative;
margin: 5px;
border: 1px solid gray;
.copy-button {
position: absolute;
top: 2px;
right: 2px;
}
}
</style>

View File

@ -0,0 +1,17 @@
<script lang="ts">
import Button from './Button/Button.svelte'
import { ButtonStyles } from './Button/types'
import Clipboard from './Clipboard.svelte'
let isCopied = false
export let code: string
export let copy: () => void
const handleCopy = () => {
isCopied = true
copy()
}
</script>
<Clipboard text={code} let:copy on:copy={handleCopy}>
<Button style={ButtonStyles.Micro} click={copy}>{isCopied ? 'Copied!' : 'Copy'}</Button>
</Clipboard>

View File

@ -0,0 +1,8 @@
<error><slot /></error>
<style type="text/scss">
error {
color: red;
display: block;
}
</style>

View File

@ -0,0 +1,16 @@
import { map } from '@s-libs/micro-dash'
import { ClientResponseError } from 'pocketbase'
export const parseError = (e) => {
if (e instanceof ClientResponseError) {
const { data } = e
if (!data || !data.data) {
return `Unknown error ${e.message}`
}
return map(data.data, (v, k) => (v ? v.message : undefined))
.filter((v) => !!v)
.join('<br/>')
} else {
return `Unknown error ${e.message}`
}
}

View File

@ -0,0 +1 @@
export type ErrorMessage = string[] | string

View File

@ -0,0 +1,31 @@
<script lang="ts">
import { isLoggedIn, logOut, user } from '@pockethost/common/src/pocketbase'
import Button from './Button/Button.svelte'
import { ButtonStyles } from './Button/types'
if (typeof window !== 'undefined' && !isLoggedIn()) {
window.location.href = '/signup'
}
const handleLogout = () => {
logOut()
window.location.href = '/'
}
</script>
<div class="logbar">
{user()?.email}
<Button style={ButtonStyles.Micro} click={handleLogout}>Log Out</Button>
<div>
<a href="/dashboard">dashboard</a>
</div>
</div>
<slot />
<style lang="scss">
.logbar {
position: absolute;
right: 0px;
top: 0px;
}
</style>

View File

@ -0,0 +1,32 @@
<script lang="ts">
import { InstanceStatuses } from '@pockethost/common/src/schema'
import { ProvisioningSize } from './types'
export let status: InstanceStatuses = InstanceStatuses.Unknown
export let size: ProvisioningSize = ProvisioningSize.Normal
</script>
<div class={`status ${status} ${size}`}>{status}</div>
<style lang="scss">
.status {
border-radius: 5px;
display: inline-block;
padding: 3px;
font-size: 10px;
background-color: rgb(217, 138, 10);
color: white;
&.started {
background-color: green;
}
&.failed {
background-color: red;
}
&.hero {
font-size: 50px;
padding: 20px;
border-radius: 20px;
border: 1px solid black;
}
}
</style>

View File

@ -0,0 +1,6 @@
export enum ProvisioningSize {
Small = 'small',
Normal = 'normal',
Large = 'large',
Hero = 'hero'
}

View File

@ -0,0 +1,22 @@
<script lang="ts">
export let first = 'Pocket'
export let second = 'Host'
export let third = ''
</script>
<h1>{first}<span id="host">{second}</span>{third}</h1>
<style type="text/scss">
h1 {
color: #ff3e00;
font-size: 12vw;
font-weight: 100;
margin-left: auto;
margin-right: auto;
display: block;
text-align: center;
#host {
color: blue;
}
}
</style>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import Title from '../components/Title.svelte'
import Button from '../components/Button/Button.svelte'
import AuthCheck from '../components/AuthCheck.svelte'
import Caption from '../components/Caption/Caption.svelte'
import { CaptionSize } from '../components/Caption/types'
</script>
<AuthCheck>
<Title third=".io" />
<main>
<Caption size={CaptionSize.Hero}>
The zero config, zero setup <a href="https://pocketbase.io">PocketBase</a>
backend for your next app.
</Caption>
<Button href="/signup">Get Started</Button>
</main>
</AuthCheck>
<style type="text/scss">
main {
text-align: center;
padding: 1em;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@ -0,0 +1,67 @@
<script lang="ts">
import { page } from '$app/stores'
import { identity } from 'ts-brand'
import Caption from '../../../../components/Caption/Caption.svelte'
import CodeSample from '../../../../components/CodeSample.svelte'
import Protected from '../../../../components/Protected.svelte'
import Title from '../../../../components/Title.svelte'
import { isBrowser } from '@pockethost/common/src/isBrowser'
import { assertExists } from '@pockethost/common/src/assert'
import { getInstanceById, watchInstanceById } from '@pockethost/common/src/pocketbase'
import {
InstanceStatuses,
type InstanceId,
type Instance_Out
} from '@pockethost/common/src/schema'
import { onDestroy, onMount } from 'svelte'
import type { Unsubscriber } from 'svelte/store'
import ProvisioningStatus from '../../../../components/ProvisioningStatus/ProvisioningStatus.svelte'
import { ProvisioningSize } from '../../../../components/ProvisioningStatus/types'
const { instanceId } = $page.params
let instance: Instance_Out | undefined
let url: string
let code: string = ''
let unsub: Unsubscriber = () => {}
onMount(() => {
unsub = watchInstanceById(identity<InstanceId>(instanceId), (r) => {
console.log(`got a record`, r)
instance = r
assertExists(instance, `Expected instance here`)
const { subdomain } = instance
url = `https://${subdomain}.pockethost.io`
code = `const url = '${url}'\nconst client = new PocketBase(url)`
})
})
onDestroy(() => unsub())
</script>
<Protected>
<Title />
{#if instance}
{#if instance.status === InstanceStatuses.Started}
<Caption>Your PocketHost instance is now live.</Caption>
<div>
Admin URL: <a href={`${url}/_`} target="_blank">{`${url}/_`}</a>
</div>
<div>
JavaScript:
<CodeSample {code} />
</div>
{/if}
{#if instance.status !== InstanceStatuses.Started}
<Caption>Please stand by, your instance is starting now...</Caption>
<div class="provisioning">
<ProvisioningStatus status={instance.status} size={ProvisioningSize.Hero} />
</div>
{/if}
{/if}
</Protected>
<style lang="scss">
.provisioning {
text-align: center;
}
</style>

View File

@ -0,0 +1,95 @@
<script lang="ts">
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
import PocketBase from 'pocketbase'
import { generateSlug } from 'random-word-slugs'
import Fa from 'svelte-fa/src/fa.svelte'
import Button from '../../../components/Button/Button.svelte'
import { ButtonStyles } from '../../../components/Button/types'
import Error from '../../../components/Error/Error.svelte'
import { parseError } from '../../../components/Error/parseError'
import Protected from '../../../components/Protected.svelte'
import Title from '../../../components/Title.svelte'
import { createInstance, user } from '@pockethost/common/src/pocketbase'
import { redirect } from '../../../util/redirect'
import { identity } from 'ts-brand'
import { assertExists } from '@pockethost/common/src/assert'
import type { Subdomain, UserId } from '@pockethost/common/src/schema'
const client = new PocketBase('https://db.pockethost.io')
if (!client.authStore.isValid && typeof window !== 'undefined') {
window.location.href = '/signup'
}
let instanceName = generateSlug(2)
let errorMessage = ''
let code = ''
$: {
code = `const url = 'https://${instanceName}.pockethost.io'\nconst client = new PocketBase(url)`
}
const handleCreate = () => {
console.log(`creating `, instanceName)
const { id } = user() || {}
assertExists<UserId>(id, `Expected uid here`)
createInstance({
subdomain: identity<Subdomain>(instanceName),
uid: id
})
.then((rec) => {
console.log(`Record`, rec)
redirect(`/app/instances/${rec.id}`)
})
.catch((e) => {
errorMessage = parseError(e)
console.error(errorMessage, e)
})
}
</script>
<Protected>
<Title />
<main>
<h2>New App</h2>
<div class="caption">Choose a name for your PocketBase app.</div>
<div class="subdomain">
<label for="instanceName">Instance Name</label>
<Button click={() => (instanceName = generateSlug(2))} style={ButtonStyles.Micro}>
<Fa icon={faRefresh} /></Button
>
<input
class="subdomain"
name="instanceName"
type="text"
bind:value={instanceName}
/>.pockethost.io
</div>
<Error>{errorMessage}</Error>
<Button click={handleCreate}>Create</Button>
</main>
</Protected>
<style type="text/scss">
main {
padding: 1em;
margin-left: auto;
margin-right: auto;
label {
display: block;
font-weight: bold;
width: 200px;
}
.caption {
font-size: 15px;
margin-top: 20px;
margin-bottom: 20px;
}
.subdomain {
input {
text-align: right;
max-width: 200px;
}
white-space: nowrap;
}
}
</style>

View File

@ -0,0 +1,57 @@
<script lang="ts">
import { getAllInstancesById } from '@pockethost/common/src/pocketbase'
import { InstanceStatuses, type Instance_Out_ByIdCollection } from '@pockethost/common/src/schema'
import { values } from '@s-libs/micro-dash'
import Button from '../../components/Button/Button.svelte'
import { ButtonStyles } from '../../components/Button/types'
import Protected from '../../components/Protected.svelte'
import Title from '../../components/Title.svelte'
import ProvisioningStatus from '../../components/ProvisioningStatus/ProvisioningStatus.svelte'
let apps: Instance_Out_ByIdCollection = {}
getAllInstancesById()
.then((instances) => {
apps = instances
})
.catch((e) => {
console.error(`Failed to fetch instances`)
})
</script>
<Protected>
<Title />
<main>
<h2>Dashboard</h2>
<h4>Apps</h4>
{#each values(apps) as app}
<div>
<ProvisioningStatus status={app.status} />
{app.subdomain}.pockethost.io
<Button style={ButtonStyles.Micro} href={`/app/instances/${app.id}`}>Details</Button>
<Button
disabled={app.status !== InstanceStatuses.Started}
style={ButtonStyles.Micro}
click={() => {
window.open(`https://${app.subdomain}.pockethost.io/_`)
}}>Admin</Button
>
</div>
{/each}
<Button href="/app/new">+</Button>
</main>
</Protected>
<style type="text/scss">
main {
padding: 1em;
margin-left: auto;
margin-right: auto;
.caption {
font-size: 30px;
margin-top: 20px;
margin-bottom: 20px;
}
}
</style>

View File

@ -0,0 +1,65 @@
<script lang="ts">
import { ClientResponseError } from 'pocketbase'
import AuthCheck from '../../components/AuthCheck.svelte'
import Button from '../../components/Button/Button.svelte'
import Title from '../../components/Title.svelte'
import { authViaEmail, createUser } from '@pockethost/common/src/pocketbase'
let email = ''
let password = ''
let loginError = ''
const handleLogin = () => {
loginError = ''
authViaEmail(email, password)
.then((user) => {
console.log(user)
window.location.href = '/dashboard'
})
.catch((e) => {
loginError = e.message
})
}
</script>
<Title first="Log" second="In" />
<main>
<error>{loginError}</error>
<div>
<label for="email">Email</label>
<input name="email" type="email" bind:value={email} />
</div>
<div>
<label for="password">Password</label>
<input name="password" type="password" bind:value={password} />
</div>
<div>
Need to <a href="/signup">create an account</a>?
</div>
<Button click={handleLogin} disabled={email.length === 0 || password.length === 0}>Log In</Button>
</main>
<style type="text/scss">
error {
color: red;
display: block;
}
label {
display: block;
font-weight: bold;
width: 200px;
}
main {
padding: 1em;
margin-left: auto;
margin-right: auto;
}
.caption {
font-size: 30px;
margin-top: 20px;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,84 @@
<script lang="ts">
import { authViaEmail, createUser } from '@pockethost/common/src/pocketbase';
import Button from '../../components/Button/Button.svelte';
import { parseError } from '../../components/Error/parseError';
import Title from '../../components/Title.svelte';
let email = '';
let errorMessage = '';
let password = '';
let passwordError = '';
// client.users
// .authViaEmail('ben@benallfree.com', 'Dhjb2X6C1y0W')
// .then((u) => {
// console.log(`user logged in`, u)
// window.location.href = '/dashboard'
// })
// .catch((e) => console.error(`user login error`, e))
const handleSignup = () => {
errorMessage = '';
passwordError = '';
createUser(email, password)
.then((user) => {
console.log({ user });
authViaEmail(email, password)
.then((u) => {
console.log(`user logged in`, u);
window.location.href = '/dashboard';
})
.catch((e) => console.error(`user login error`, e));
})
.catch((e) => {
errorMessage = parseError(e);
console.error(errorMessage, e);
});
};
</script>
<Title first="Sign" second="Up" />
<main>
<div>
<label for="email">Email</label>
<input name="email" type="email" bind:value={email} />
<error>{errorMessage}</error>
</div>
<div>
<label for="password">Password</label>
<input name="password" type="password" bind:value={password} />
<error>{passwordError}</error>
</div>
<Button click={handleSignup} disabled={email.length === 0 || password.length === 0}>
Sign Up
</Button>
<div>
Already have an account? <a href="/login">Log in</a>
</div>
</main>
<style type="text/scss">
error {
color: red;
display: block;
}
label {
display: block;
font-weight: bold;
width: 200px;
}
main {
padding: 1em;
margin-left: auto;
margin-right: auto;
}
.caption {
font-size: 30px;
margin-top: 20px;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,4 @@
export type MessageField = {
code: string
message: string
}

View File

@ -0,0 +1,4 @@
export const redirect = (url: string) => {
if (typeof window === 'undefined') return
window.location.href = url
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,63 @@
html, body {
position: relative;
width: 100%;
height: 100%;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}

View File

@ -0,0 +1,14 @@
import adapter from '@sveltejs/adapter-node'
import preprocess from 'svelte-preprocess'
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter: adapter({ out: 'dist-server' }) },
}
export default config

View File

@ -0,0 +1,18 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"baseUrl": "",
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite'
import type { UserConfig } from 'vite'
const config: UserConfig = {
plugins: [sveltekit()],
optimizeDeps: {
include: ['highlight.js', 'highlight.js/lib/core'],
},
}
export default config

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2804
yarn.lock Normal file

File diff suppressed because it is too large Load Diff