-14
.sqlx/query-076cbf7f32c5f0103207a8e0e73dd5768681ff2520682edda8f2977dcae7cd62.json
-14
.sqlx/query-076cbf7f32c5f0103207a8e0e73dd5768681ff2520682edda8f2977dcae7cd62.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity')",
4
-
"describe": {
5
-
"columns": [],
6
-
"parameters": {
7
-
"Left": [
8
-
"Text"
9
-
]
10
-
},
11
-
"nullable": []
12
-
},
13
-
"hash": "076cbf7f32c5f0103207a8e0e73dd5768681ff2520682edda8f2977dcae7cd62"
14
-
}
-28
.sqlx/query-1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a.json
-28
.sqlx/query-1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "SELECT handle, email FROM users WHERE did = $1",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "handle",
9
-
"type_info": "Text"
10
-
},
11
-
{
12
-
"ordinal": 1,
13
-
"name": "email",
14
-
"type_info": "Text"
15
-
}
16
-
],
17
-
"parameters": {
18
-
"Left": [
19
-
"Text"
20
-
]
21
-
},
22
-
"nullable": [
23
-
false,
24
-
true
25
-
]
26
-
},
27
-
"hash": "1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a"
28
-
}
+70
.sqlx/query-3727afe601beffcb4551c23eb59fa1c25ca59150c90d4866050b3a073bbe7b4b.json
+70
.sqlx/query-3727afe601beffcb4551c23eb59fa1c25ca59150c90d4866050b3a073bbe7b4b.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT\n handle, email, email_confirmed,\n preferred_notification_channel as \"preferred_channel: crate::notifications::NotificationChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "handle",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "email",
14
+
"type_info": "Text"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "email_confirmed",
19
+
"type_info": "Bool"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "preferred_channel: crate::notifications::NotificationChannel",
24
+
"type_info": {
25
+
"Custom": {
26
+
"name": "notification_channel",
27
+
"kind": {
28
+
"Enum": [
29
+
"email",
30
+
"discord",
31
+
"telegram",
32
+
"signal"
33
+
]
34
+
}
35
+
}
36
+
}
37
+
},
38
+
{
39
+
"ordinal": 4,
40
+
"name": "discord_verified",
41
+
"type_info": "Bool"
42
+
},
43
+
{
44
+
"ordinal": 5,
45
+
"name": "telegram_verified",
46
+
"type_info": "Bool"
47
+
},
48
+
{
49
+
"ordinal": 6,
50
+
"name": "signal_verified",
51
+
"type_info": "Bool"
52
+
}
53
+
],
54
+
"parameters": {
55
+
"Left": [
56
+
"Text"
57
+
]
58
+
},
59
+
"nullable": [
60
+
false,
61
+
true,
62
+
false,
63
+
false,
64
+
false,
65
+
false,
66
+
false
67
+
]
68
+
},
69
+
"hash": "3727afe601beffcb4551c23eb59fa1c25ca59150c90d4866050b3a073bbe7b4b"
70
+
}
+22
.sqlx/query-95165c49f57bb8e130bf1d8444566e1f9521777f994573a4f3cdee809fb63fd7.json
+22
.sqlx/query-95165c49f57bb8e130bf1d8444566e1f9521777f994573a4f3cdee809fb63fd7.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "seq",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "95165c49f57bb8e130bf1d8444566e1f9521777f994573a4f3cdee809fb63fd7"
22
+
}
-19
.sqlx/query-aadc1f8c79d79e9a32fe6f4bf7e901076532fa2bf8f0b4d0f1bae7aa0f792183.json
-19
.sqlx/query-aadc1f8c79d79e9a32fe6f4bf7e901076532fa2bf8f0b4d0f1bae7aa0f792183.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)\n VALUES ($1, 'commit', $2, $3, $4, $5, $6)\n ",
4
-
"describe": {
5
-
"columns": [],
6
-
"parameters": {
7
-
"Left": [
8
-
"Text",
9
-
"Text",
10
-
"Text",
11
-
"Jsonb",
12
-
"TextArray",
13
-
"TextArray"
14
-
]
15
-
},
16
-
"nullable": []
17
-
},
18
-
"hash": "aadc1f8c79d79e9a32fe6f4bf7e901076532fa2bf8f0b4d0f1bae7aa0f792183"
19
-
}
+12
-6
.sqlx/query-cab71411113374c8c388a35281c676b3822629d505e84d51b60162e80a43d190.json
.sqlx/query-257aa21927a280477b9b59ad726a67a587a0164a38665594d4925b11bf2b260b.json
+12
-6
.sqlx/query-cab71411113374c8c388a35281c676b3822629d505e84d51b60162e80a43d190.json
.sqlx/query-257aa21927a280477b9b59ad726a67a587a0164a38665594d4925b11bf2b260b.json
···
1
1
{
2
2
"db_name": "PostgreSQL",
3
-
"query": "SELECT\n u.id, u.did, u.handle,\n u.email_confirmation_code,\n u.email_confirmation_code_expires_at,\n u.preferred_notification_channel as \"channel: crate::notifications::NotificationChannel\",\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1",
3
+
"query": "SELECT\n u.id, u.did, u.handle, u.email,\n u.email_confirmation_code,\n u.email_confirmation_code_expires_at,\n u.preferred_notification_channel as \"channel: crate::notifications::NotificationChannel\",\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1",
4
4
"describe": {
5
5
"columns": [
6
6
{
···
20
20
},
21
21
{
22
22
"ordinal": 3,
23
+
"name": "email",
24
+
"type_info": "Text"
25
+
},
26
+
{
27
+
"ordinal": 4,
23
28
"name": "email_confirmation_code",
24
29
"type_info": "Text"
25
30
},
26
31
{
27
-
"ordinal": 4,
32
+
"ordinal": 5,
28
33
"name": "email_confirmation_code_expires_at",
29
34
"type_info": "Timestamptz"
30
35
},
31
36
{
32
-
"ordinal": 5,
37
+
"ordinal": 6,
33
38
"name": "channel: crate::notifications::NotificationChannel",
34
39
"type_info": {
35
40
"Custom": {
···
46
51
}
47
52
},
48
53
{
49
-
"ordinal": 6,
54
+
"ordinal": 7,
50
55
"name": "key_bytes",
51
56
"type_info": "Bytea"
52
57
},
53
58
{
54
-
"ordinal": 7,
59
+
"ordinal": 8,
55
60
"name": "encryption_version",
56
61
"type_info": "Int4"
57
62
}
···
67
72
false,
68
73
true,
69
74
true,
75
+
true,
70
76
false,
71
77
false,
72
78
true
73
79
]
74
80
},
75
-
"hash": "cab71411113374c8c388a35281c676b3822629d505e84d51b60162e80a43d190"
81
+
"hash": "257aa21927a280477b9b59ad726a67a587a0164a38665594d4925b11bf2b260b"
76
82
}
+27
.sqlx/query-f68a05d2c78cc060b43c81b177a24f89c71e7e00dfa5af08ff4584bbe43b4155.json
+27
.sqlx/query-f68a05d2c78cc060b43c81b177a24f89c71e7e00dfa5af08ff4584bbe43b4155.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)\n VALUES ($1, 'commit', $2, $3, $4, $5, $6)\n RETURNING seq\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "seq",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text",
15
+
"Text",
16
+
"Text",
17
+
"Jsonb",
18
+
"TextArray",
19
+
"TextArray"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false
24
+
]
25
+
},
26
+
"hash": "f68a05d2c78cc060b43c81b177a24f89c71e7e00dfa5af08ff4584bbe43b4155"
27
+
}
+1
-1
docs/install-alpine.md
+1
-1
docs/install-alpine.md
+2
-2
docs/install-containers.md
+2
-2
docs/install-containers.md
···
92
92
93
93
```bash
94
94
cd /opt
95
-
git clone https://tangled.org/lewis.moe/bspds.git
95
+
git clone https://tangled.org/lewis.moe/bspds-sandbox bspds
96
96
cd bspds
97
97
podman build -t bspds:latest .
98
98
```
···
208
208
209
209
```sh
210
210
cd /opt
211
-
git clone https://tangled.org/lewis.moe/bspds.git
211
+
git clone https://tangled.org/lewis.moe/bspds-sandbox bspds
212
212
cd bspds
213
213
podman build -t bspds:latest .
214
214
```
+1
-1
docs/install-debian.md
+1
-1
docs/install-debian.md
+1
-1
docs/install-openbsd.md
+1
-1
docs/install-openbsd.md
+6
frontend/src/lib/api.ts
+6
frontend/src/lib/api.ts
···
51
51
handle: string
52
52
email?: string
53
53
emailConfirmed?: boolean
54
+
preferredChannel?: string
55
+
preferredChannelVerified?: boolean
54
56
accessJwt: string
55
57
refreshJwt: string
56
58
}
···
95
97
refreshJwt: string
96
98
handle: string
97
99
did: string
100
+
email?: string
101
+
emailConfirmed?: boolean
102
+
preferredChannel?: string
103
+
preferredChannelVerified?: boolean
98
104
}
99
105
100
106
export const api = {
+4
frontend/src/lib/auth.svelte.ts
+4
frontend/src/lib/auth.svelte.ts
···
108
108
handle: result.handle,
109
109
accessJwt: result.accessJwt,
110
110
refreshJwt: result.refreshJwt,
111
+
email: result.email,
112
+
emailConfirmed: result.emailConfirmed,
113
+
preferredChannel: result.preferredChannel,
114
+
preferredChannelVerified: result.preferredChannelVerified,
111
115
}
112
116
state.session = session
113
117
saveSession(session)
+21
-1
frontend/src/routes/Dashboard.svelte
+21
-1
frontend/src/routes/Dashboard.svelte
···
32
32
<dt>DID</dt>
33
33
<dd class="mono">{auth.session.did}</dd>
34
34
35
-
{#if auth.session.email}
35
+
{#if auth.session.preferredChannel}
36
+
<dt>Primary Contact</dt>
37
+
<dd>
38
+
{#if auth.session.preferredChannel === 'email'}
39
+
{auth.session.email || 'Email'}
40
+
{:else if auth.session.preferredChannel === 'discord'}
41
+
Discord
42
+
{:else if auth.session.preferredChannel === 'telegram'}
43
+
Telegram
44
+
{:else if auth.session.preferredChannel === 'signal'}
45
+
Signal
46
+
{:else}
47
+
{auth.session.preferredChannel}
48
+
{/if}
49
+
{#if auth.session.preferredChannelVerified}
50
+
<span class="badge success">Verified</span>
51
+
{:else}
52
+
<span class="badge warning">Unverified</span>
53
+
{/if}
54
+
</dd>
55
+
{:else if auth.session.email}
36
56
<dt>Email</dt>
37
57
<dd>
38
58
{auth.session.email}
+1
-1
frontend/src/routes/Register.svelte
+1
-1
frontend/src/routes/Register.svelte
+645
scripts/install-debian.sh
+645
scripts/install-debian.sh
···
1
+
#!/bin/bash
2
+
set -euo pipefail
3
+
4
+
RED='\033[0;31m'
5
+
GREEN='\033[0;32m'
6
+
YELLOW='\033[1;33m'
7
+
BLUE='\033[0;34m'
8
+
CYAN='\033[0;36m'
9
+
NC='\033[0m'
10
+
11
+
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
12
+
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
13
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
14
+
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
15
+
16
+
if [[ $EUID -ne 0 ]]; then
17
+
log_error "This script must be run as root"
18
+
exit 1
19
+
fi
20
+
21
+
if ! grep -qi "debian" /etc/os-release 2>/dev/null; then
22
+
log_warn "This script is designed for Debian. Proceed with caution on other distros."
23
+
fi
24
+
25
+
echo -e "${CYAN}"
26
+
echo "╔═══════════════════════════════════════════════════════════════════╗"
27
+
echo "║ BSPDS Installation Script for Debian ║"
28
+
echo "║ AT Protocol Personal Data Server in Rust ║"
29
+
echo "╚═══════════════════════════════════════════════════════════════════╝"
30
+
echo -e "${NC}"
31
+
32
+
get_public_ips() {
33
+
IPV4=$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 icanhazip.com 2>/dev/null || echo "Could not detect")
34
+
IPV6=$(curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 icanhazip.com 2>/dev/null || echo "Not available")
35
+
}
36
+
37
+
log_info "Detecting public IP addresses..."
38
+
get_public_ips
39
+
40
+
echo ""
41
+
echo -e "${CYAN}Your server's public IPs:${NC}"
42
+
echo -e " IPv4: ${GREEN}${IPV4}${NC}"
43
+
echo -e " IPv6: ${GREEN}${IPV6}${NC}"
44
+
echo ""
45
+
46
+
read -p "Enter your PDS domain (e.g., pds.example.com): " PDS_DOMAIN
47
+
if [[ -z "$PDS_DOMAIN" ]]; then
48
+
log_error "Domain cannot be empty"
49
+
exit 1
50
+
fi
51
+
52
+
read -p "Enter your email for Let's Encrypt notifications: " CERTBOT_EMAIL
53
+
if [[ -z "$CERTBOT_EMAIL" ]]; then
54
+
log_error "Email cannot be empty"
55
+
exit 1
56
+
fi
57
+
58
+
echo ""
59
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
60
+
echo -e "${YELLOW}DNS RECORDS REQUIRED${NC}"
61
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
62
+
echo ""
63
+
echo "Before continuing, create these DNS records at your registrar:"
64
+
echo ""
65
+
echo -e "${GREEN}A Record:${NC}"
66
+
echo " Name: ${PDS_DOMAIN}"
67
+
echo " Type: A"
68
+
echo " Value: ${IPV4}"
69
+
echo ""
70
+
if [[ "$IPV6" != "Not available" ]]; then
71
+
echo -e "${GREEN}AAAA Record:${NC}"
72
+
echo " Name: ${PDS_DOMAIN}"
73
+
echo " Type: AAAA"
74
+
echo " Value: ${IPV6}"
75
+
echo ""
76
+
fi
77
+
echo -e "${GREEN}Wildcard A Record (for user handles):${NC}"
78
+
echo " Name: *.${PDS_DOMAIN}"
79
+
echo " Type: A"
80
+
echo " Value: ${IPV4}"
81
+
echo ""
82
+
if [[ "$IPV6" != "Not available" ]]; then
83
+
echo -e "${GREEN}Wildcard AAAA Record (for user handles):${NC}"
84
+
echo " Name: *.${PDS_DOMAIN}"
85
+
echo " Type: AAAA"
86
+
echo " Value: ${IPV6}"
87
+
echo ""
88
+
fi
89
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
90
+
echo ""
91
+
read -p "Have you created these DNS records? (y/N): " DNS_CONFIRMED
92
+
if [[ ! "$DNS_CONFIRMED" =~ ^[Yy]$ ]]; then
93
+
log_warn "Please create the DNS records and run this script again."
94
+
exit 0
95
+
fi
96
+
97
+
CREDENTIALS_FILE="/etc/bspds/.credentials"
98
+
99
+
if [[ -f "$CREDENTIALS_FILE" ]]; then
100
+
log_info "Loading existing credentials from previous installation..."
101
+
source "$CREDENTIALS_FILE"
102
+
log_success "Credentials loaded"
103
+
else
104
+
log_info "Generating secure secrets..."
105
+
JWT_SECRET=$(openssl rand -base64 48)
106
+
DPOP_SECRET=$(openssl rand -base64 48)
107
+
MASTER_KEY=$(openssl rand -base64 48)
108
+
DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
109
+
MINIO_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
110
+
111
+
mkdir -p /etc/bspds
112
+
cat > "$CREDENTIALS_FILE" << EOF
113
+
JWT_SECRET="$JWT_SECRET"
114
+
DPOP_SECRET="$DPOP_SECRET"
115
+
MASTER_KEY="$MASTER_KEY"
116
+
DB_PASSWORD="$DB_PASSWORD"
117
+
MINIO_PASSWORD="$MINIO_PASSWORD"
118
+
EOF
119
+
chmod 600 "$CREDENTIALS_FILE"
120
+
log_success "Secrets generated and saved"
121
+
fi
122
+
123
+
log_info "Checking swap space..."
124
+
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
125
+
TOTAL_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
126
+
127
+
if [[ $TOTAL_SWAP_KB -lt 2000000 ]]; then
128
+
log_info "Adding swap space (needed for compilation)..."
129
+
if [[ ! -f /swapfile ]]; then
130
+
SWAP_SIZE="4G"
131
+
if [[ $TOTAL_MEM_KB -lt 2000000 ]]; then
132
+
SWAP_SIZE="4G"
133
+
elif [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
134
+
SWAP_SIZE="2G"
135
+
fi
136
+
fallocate -l $SWAP_SIZE /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=4096
137
+
chmod 600 /swapfile
138
+
mkswap /swapfile
139
+
swapon /swapfile
140
+
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
141
+
log_success "Swap space added ($SWAP_SIZE)"
142
+
else
143
+
swapon /swapfile 2>/dev/null || true
144
+
log_success "Existing swap enabled"
145
+
fi
146
+
else
147
+
log_success "Sufficient swap already configured"
148
+
fi
149
+
150
+
log_info "Updating system packages..."
151
+
apt update && apt upgrade -y
152
+
log_success "System updated"
153
+
154
+
log_info "Installing build dependencies..."
155
+
apt install -y curl git build-essential pkg-config libssl-dev ca-certificates gnupg lsb-release unzip xxd
156
+
log_success "Build dependencies installed"
157
+
158
+
log_info "Installing postgres..."
159
+
apt install -y postgresql postgresql-contrib
160
+
systemctl enable postgresql
161
+
systemctl start postgresql
162
+
163
+
sudo -u postgres psql -c "CREATE USER bspds WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || \
164
+
sudo -u postgres psql -c "ALTER USER bspds WITH PASSWORD '${DB_PASSWORD}';"
165
+
sudo -u postgres psql -c "CREATE DATABASE pds OWNER bspds;" 2>/dev/null || true
166
+
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;"
167
+
log_success "postgres installed and configured"
168
+
169
+
log_info "Installing valkey..."
170
+
apt install -y valkey || {
171
+
log_warn "valkey not in repos, trying redis..."
172
+
apt install -y redis-server
173
+
systemctl enable redis-server
174
+
systemctl start redis-server
175
+
}
176
+
systemctl enable valkey-server 2>/dev/null || true
177
+
systemctl start valkey-server 2>/dev/null || true
178
+
log_success "valkey/redis installed"
179
+
180
+
log_info "Installing minio..."
181
+
if [[ ! -f /usr/local/bin/minio ]]; then
182
+
ARCH=$(dpkg --print-architecture)
183
+
if [[ "$ARCH" == "amd64" ]]; then
184
+
curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-amd64/minio
185
+
elif [[ "$ARCH" == "arm64" ]]; then
186
+
curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-arm64/minio
187
+
else
188
+
log_error "Unsupported architecture: $ARCH"
189
+
exit 1
190
+
fi
191
+
chmod +x /tmp/minio
192
+
mv /tmp/minio /usr/local/bin/
193
+
fi
194
+
195
+
mkdir -p /var/lib/minio/data
196
+
id -u minio-user &>/dev/null || useradd -r -s /sbin/nologin minio-user
197
+
chown -R minio-user:minio-user /var/lib/minio
198
+
199
+
cat > /etc/default/minio << EOF
200
+
MINIO_ROOT_USER=minioadmin
201
+
MINIO_ROOT_PASSWORD=${MINIO_PASSWORD}
202
+
MINIO_VOLUMES="/var/lib/minio/data"
203
+
MINIO_OPTS="--console-address :9001"
204
+
EOF
205
+
chmod 600 /etc/default/minio
206
+
207
+
cat > /etc/systemd/system/minio.service << 'EOF'
208
+
[Unit]
209
+
Description=MinIO Object Storage
210
+
After=network.target
211
+
212
+
[Service]
213
+
User=minio-user
214
+
Group=minio-user
215
+
EnvironmentFile=/etc/default/minio
216
+
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
217
+
Restart=always
218
+
LimitNOFILE=65536
219
+
220
+
[Install]
221
+
WantedBy=multi-user.target
222
+
EOF
223
+
224
+
systemctl daemon-reload
225
+
systemctl enable minio
226
+
systemctl start minio
227
+
log_success "minio installed"
228
+
229
+
log_info "Waiting for minio to start..."
230
+
sleep 5
231
+
232
+
log_info "Installing minio client and creating bucket..."
233
+
if [[ ! -f /usr/local/bin/mc ]]; then
234
+
ARCH=$(dpkg --print-architecture)
235
+
if [[ "$ARCH" == "amd64" ]]; then
236
+
curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc
237
+
elif [[ "$ARCH" == "arm64" ]]; then
238
+
curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-arm64/mc
239
+
fi
240
+
chmod +x /tmp/mc
241
+
mv /tmp/mc /usr/local/bin/
242
+
fi
243
+
244
+
mc alias remove local 2>/dev/null || true
245
+
mc alias set local http://localhost:9000 minioadmin "${MINIO_PASSWORD}" --api S3v4
246
+
mc mb local/pds-blobs --ignore-existing
247
+
log_success "minio bucket created"
248
+
249
+
log_info "Installing rust..."
250
+
if [[ -f "$HOME/.cargo/env" ]]; then
251
+
source "$HOME/.cargo/env"
252
+
fi
253
+
if ! command -v rustc &>/dev/null; then
254
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
255
+
source "$HOME/.cargo/env"
256
+
fi
257
+
log_success "rust installed"
258
+
259
+
log_info "Installing deno..."
260
+
export PATH="$HOME/.deno/bin:$PATH"
261
+
if ! command -v deno &>/dev/null && [[ ! -f "$HOME/.deno/bin/deno" ]]; then
262
+
curl -fsSL https://deno.land/install.sh | sh
263
+
grep -q 'deno/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
264
+
fi
265
+
log_success "deno installed"
266
+
267
+
log_info "Cloning BSPDS..."
268
+
if [[ ! -d /opt/bspds ]]; then
269
+
git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/bspds
270
+
else
271
+
log_warn "/opt/bspds already exists, pulling latest..."
272
+
cd /opt/bspds && git pull
273
+
fi
274
+
cd /opt/bspds
275
+
log_success "BSPDS cloned"
276
+
277
+
log_info "Building frontend..."
278
+
cd /opt/bspds/frontend
279
+
"$HOME/.deno/bin/deno" task build
280
+
cd /opt/bspds
281
+
log_success "Frontend built"
282
+
283
+
log_info "Building BSPDS (this may take a while)..."
284
+
source "$HOME/.cargo/env"
285
+
NPROC=$(nproc)
286
+
if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
287
+
log_info "Low memory detected, limiting parallel jobs..."
288
+
CARGO_BUILD_JOBS=1 cargo build --release
289
+
else
290
+
cargo build --release
291
+
fi
292
+
log_success "BSPDS built"
293
+
294
+
log_info "Installing sqlx-cli and running migrations..."
295
+
cargo install sqlx-cli --no-default-features --features postgres
296
+
export DATABASE_URL="postgres://bspds:${DB_PASSWORD}@localhost:5432/pds"
297
+
"$HOME/.cargo/bin/sqlx" migrate run
298
+
log_success "Migrations complete"
299
+
300
+
log_info "Setting up mail trap for testing..."
301
+
mkdir -p /var/spool/bspds-mail
302
+
chown root:root /var/spool/bspds-mail
303
+
chmod 1777 /var/spool/bspds-mail
304
+
305
+
cat > /usr/local/bin/bspds-sendmail << 'SENDMAIL_EOF'
306
+
#!/bin/bash
307
+
MAIL_DIR="/var/spool/bspds-mail"
308
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
309
+
RANDOM_ID=$(head -c 4 /dev/urandom | xxd -p)
310
+
MAIL_FILE="${MAIL_DIR}/${TIMESTAMP}-${RANDOM_ID}.eml"
311
+
312
+
mkdir -p "$MAIL_DIR"
313
+
314
+
{
315
+
echo "X-BSPDS-Received: $(date -Iseconds)"
316
+
echo "X-BSPDS-Args: $*"
317
+
echo ""
318
+
cat
319
+
} > "$MAIL_FILE"
320
+
321
+
chmod 644 "$MAIL_FILE"
322
+
echo "Mail saved to: $MAIL_FILE" >&2
323
+
exit 0
324
+
SENDMAIL_EOF
325
+
chmod +x /usr/local/bin/bspds-sendmail
326
+
327
+
cat > /usr/local/bin/bspds-mailq << 'MAILQ_EOF'
328
+
#!/bin/bash
329
+
MAIL_DIR="/var/spool/bspds-mail"
330
+
RED='\033[0;31m'
331
+
GREEN='\033[0;32m'
332
+
YELLOW='\033[1;33m'
333
+
BLUE='\033[0;34m'
334
+
CYAN='\033[0;36m'
335
+
NC='\033[0m'
336
+
337
+
show_help() {
338
+
echo "bspds-mailq - View captured emails from BSPDS mail trap"
339
+
echo ""
340
+
echo "Usage:"
341
+
echo " bspds-mailq List all captured emails"
342
+
echo " bspds-mailq <number> View email by number (from list)"
343
+
echo " bspds-mailq <filename> View email by filename"
344
+
echo " bspds-mailq latest View the most recent email"
345
+
echo " bspds-mailq clear Delete all captured emails"
346
+
echo " bspds-mailq watch Watch for new emails (tail -f style)"
347
+
echo " bspds-mailq count Show count of emails in queue"
348
+
echo ""
349
+
}
350
+
351
+
list_emails() {
352
+
if [[ ! -d "$MAIL_DIR" ]] || [[ -z "$(ls -A "$MAIL_DIR" 2>/dev/null)" ]]; then
353
+
echo -e "${YELLOW}No emails in queue.${NC}"
354
+
return
355
+
fi
356
+
357
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
358
+
echo -e "${GREEN} BSPDS Mail Queue${NC}"
359
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
360
+
echo ""
361
+
362
+
local i=1
363
+
for f in $(ls -t "$MAIL_DIR"/*.eml 2>/dev/null); do
364
+
local filename=$(basename "$f")
365
+
local received=$(grep "^X-BSPDS-Received:" "$f" 2>/dev/null | cut -d' ' -f2-)
366
+
local to=$(grep -i "^To:" "$f" 2>/dev/null | head -1 | cut -d' ' -f2-)
367
+
local subject=$(grep -i "^Subject:" "$f" 2>/dev/null | head -1 | sed 's/^Subject: *//')
368
+
369
+
echo -e "${BLUE}[$i]${NC} ${filename}"
370
+
echo -e " To: ${GREEN}${to:-unknown}${NC}"
371
+
echo -e " Subject: ${YELLOW}${subject:-<no subject>}${NC}"
372
+
echo -e " Received: ${received:-unknown}"
373
+
echo ""
374
+
((i++))
375
+
done
376
+
377
+
echo -e "${CYAN}Total: $((i-1)) email(s)${NC}"
378
+
}
379
+
380
+
view_email() {
381
+
local target="$1"
382
+
local file=""
383
+
384
+
if [[ "$target" == "latest" ]]; then
385
+
file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1)
386
+
elif [[ "$target" =~ ^[0-9]+$ ]]; then
387
+
file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${target}p")
388
+
elif [[ -f "$MAIL_DIR/$target" ]]; then
389
+
file="$MAIL_DIR/$target"
390
+
elif [[ -f "$target" ]]; then
391
+
file="$target"
392
+
fi
393
+
394
+
if [[ -z "$file" ]] || [[ ! -f "$file" ]]; then
395
+
echo -e "${RED}Email not found: $target${NC}"
396
+
return 1
397
+
fi
398
+
399
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
400
+
echo -e "${GREEN} $(basename "$file")${NC}"
401
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
402
+
cat "$file"
403
+
echo ""
404
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
405
+
}
406
+
407
+
clear_queue() {
408
+
local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
409
+
if [[ "$count" -eq 0 ]]; then
410
+
echo -e "${YELLOW}Queue is already empty.${NC}"
411
+
return
412
+
fi
413
+
414
+
rm -f "$MAIL_DIR"/*.eml
415
+
echo -e "${GREEN}Cleared $count email(s) from queue.${NC}"
416
+
}
417
+
418
+
watch_queue() {
419
+
echo -e "${CYAN}Watching for new emails... (Ctrl+C to stop)${NC}"
420
+
echo ""
421
+
422
+
local last_count=0
423
+
while true; do
424
+
local current_count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
425
+
if [[ "$current_count" -gt "$last_count" ]]; then
426
+
echo -e "${GREEN}[$(date +%H:%M:%S)] New email received!${NC}"
427
+
view_email latest
428
+
last_count=$current_count
429
+
fi
430
+
sleep 1
431
+
done
432
+
}
433
+
434
+
count_queue() {
435
+
local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
436
+
echo "$count"
437
+
}
438
+
439
+
case "${1:-}" in
440
+
""|list)
441
+
list_emails
442
+
;;
443
+
latest|[0-9]*)
444
+
view_email "$1"
445
+
;;
446
+
clear)
447
+
clear_queue
448
+
;;
449
+
watch)
450
+
watch_queue
451
+
;;
452
+
count)
453
+
count_queue
454
+
;;
455
+
help|--help|-h)
456
+
show_help
457
+
;;
458
+
*)
459
+
if [[ -f "$MAIL_DIR/$1" ]] || [[ -f "$1" ]]; then
460
+
view_email "$1"
461
+
else
462
+
echo -e "${RED}Unknown command: $1${NC}"
463
+
show_help
464
+
exit 1
465
+
fi
466
+
;;
467
+
esac
468
+
MAILQ_EOF
469
+
chmod +x /usr/local/bin/bspds-mailq
470
+
log_success "Mail trap configured"
471
+
472
+
log_info "Creating BSPDS configuration..."
473
+
mkdir -p /etc/bspds
474
+
475
+
cat > /etc/bspds/bspds.env << EOF
476
+
SERVER_HOST=127.0.0.1
477
+
SERVER_PORT=3000
478
+
PDS_HOSTNAME=${PDS_DOMAIN}
479
+
480
+
DATABASE_URL=postgres://bspds:${DB_PASSWORD}@localhost:5432/pds
481
+
DATABASE_MAX_CONNECTIONS=100
482
+
DATABASE_MIN_CONNECTIONS=10
483
+
484
+
S3_ENDPOINT=http://localhost:9000
485
+
AWS_REGION=us-east-1
486
+
S3_BUCKET=pds-blobs
487
+
AWS_ACCESS_KEY_ID=minioadmin
488
+
AWS_SECRET_ACCESS_KEY=${MINIO_PASSWORD}
489
+
490
+
VALKEY_URL=redis://localhost:6379
491
+
492
+
JWT_SECRET=${JWT_SECRET}
493
+
DPOP_SECRET=${DPOP_SECRET}
494
+
MASTER_KEY=${MASTER_KEY}
495
+
496
+
PLC_DIRECTORY_URL=https://plc.directory
497
+
APPVIEW_URL=https://api.bsky.app
498
+
CRAWLERS=https://bsky.network
499
+
500
+
AVAILABLE_USER_DOMAINS=${PDS_DOMAIN}
501
+
502
+
MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN}
503
+
MAIL_FROM_NAME=BSPDS
504
+
SENDMAIL_PATH=/usr/local/bin/bspds-sendmail
505
+
EOF
506
+
chmod 600 /etc/bspds/bspds.env
507
+
log_success "Configuration created"
508
+
509
+
log_info "Creating BSPDS service user..."
510
+
id -u bspds &>/dev/null || useradd -r -s /sbin/nologin bspds
511
+
512
+
cp /opt/bspds/target/release/bspds /usr/local/bin/
513
+
mkdir -p /var/lib/bspds
514
+
cp -r /opt/bspds/frontend/dist /var/lib/bspds/frontend
515
+
chown -R bspds:bspds /var/lib/bspds
516
+
log_success "BSPDS binary installed"
517
+
518
+
log_info "Creating systemd service..."
519
+
cat > /etc/systemd/system/bspds.service << 'EOF'
520
+
[Unit]
521
+
Description=BSPDS - AT Protocol PDS
522
+
After=network.target postgresql.service minio.service
523
+
524
+
[Service]
525
+
Type=simple
526
+
User=bspds
527
+
Group=bspds
528
+
EnvironmentFile=/etc/bspds/bspds.env
529
+
Environment=FRONTEND_DIR=/var/lib/bspds/frontend
530
+
ExecStart=/usr/local/bin/bspds
531
+
Restart=always
532
+
RestartSec=5
533
+
534
+
[Install]
535
+
WantedBy=multi-user.target
536
+
EOF
537
+
538
+
systemctl daemon-reload
539
+
systemctl enable bspds
540
+
systemctl start bspds
541
+
log_success "BSPDS service created and started"
542
+
543
+
log_info "Installing nginx..."
544
+
apt install -y nginx certbot python3-certbot-nginx
545
+
log_success "nginx installed"
546
+
547
+
log_info "Configuring nginx..."
548
+
cat > /etc/nginx/sites-available/bspds << EOF
549
+
server {
550
+
listen 80;
551
+
listen [::]:80;
552
+
server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
553
+
554
+
location / {
555
+
proxy_pass http://127.0.0.1:3000;
556
+
proxy_http_version 1.1;
557
+
proxy_set_header Upgrade \$http_upgrade;
558
+
proxy_set_header Connection "upgrade";
559
+
proxy_set_header Host \$host;
560
+
proxy_set_header X-Real-IP \$remote_addr;
561
+
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
562
+
proxy_set_header X-Forwarded-Proto \$scheme;
563
+
proxy_read_timeout 86400;
564
+
proxy_send_timeout 86400;
565
+
client_max_body_size 100M;
566
+
}
567
+
}
568
+
EOF
569
+
570
+
ln -sf /etc/nginx/sites-available/bspds /etc/nginx/sites-enabled/
571
+
rm -f /etc/nginx/sites-enabled/default
572
+
nginx -t
573
+
systemctl reload nginx
574
+
log_success "nginx configured"
575
+
576
+
log_info "Configuring firewall (ufw)..."
577
+
apt install -y ufw
578
+
ufw --force reset
579
+
580
+
ufw default deny incoming
581
+
ufw default allow outgoing
582
+
583
+
ufw allow ssh comment 'SSH'
584
+
ufw allow 80/tcp comment 'HTTP'
585
+
ufw allow 443/tcp comment 'HTTPS'
586
+
587
+
ufw --force enable
588
+
log_success "Firewall configured"
589
+
590
+
log_info "Obtaining SSL certificate..."
591
+
certbot --nginx -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive || {
592
+
log_warn "Wildcard cert failed (requires DNS challenge). Trying single domain..."
593
+
certbot --nginx -d "${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive
594
+
}
595
+
log_success "SSL certificate obtained"
596
+
597
+
log_info "Verifying installation..."
598
+
sleep 3
599
+
if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then
600
+
log_success "BSPDS is responding!"
601
+
else
602
+
log_warn "BSPDS may still be starting up. Check: journalctl -u bspds -f"
603
+
fi
604
+
605
+
echo ""
606
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
607
+
echo -e "${GREEN} INSTALLATION COMPLETE!${NC}"
608
+
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
609
+
echo ""
610
+
echo -e "Your PDS is now running at: ${GREEN}https://${PDS_DOMAIN}${NC}"
611
+
echo ""
612
+
echo -e "${YELLOW}IMPORTANT: Save these credentials securely!${NC}"
613
+
echo ""
614
+
echo "Database password: ${DB_PASSWORD}"
615
+
echo "MinIO password: ${MINIO_PASSWORD}"
616
+
echo ""
617
+
echo "Configuration file: /etc/bspds/bspds.env"
618
+
echo ""
619
+
echo -e "${CYAN}Useful commands:${NC}"
620
+
echo " journalctl -u bspds -f # View BSPDS logs"
621
+
echo " systemctl status bspds # Check BSPDS status"
622
+
echo " systemctl restart bspds # Restart BSPDS"
623
+
echo " curl https://${PDS_DOMAIN}/xrpc/_health # Health check"
624
+
echo ""
625
+
echo -e "${CYAN}Mail queue (for testing):${NC}"
626
+
echo " bspds-mailq # List all captured emails"
627
+
echo " bspds-mailq latest # View most recent email"
628
+
echo " bspds-mailq 1 # View email #1 from list"
629
+
echo " bspds-mailq watch # Watch for new emails live"
630
+
echo " bspds-mailq clear # Clear all captured emails"
631
+
echo ""
632
+
echo " Emails are saved to: /var/spool/bspds-mail/"
633
+
echo ""
634
+
echo -e "${CYAN}DNS Records Summary:${NC}"
635
+
echo ""
636
+
echo " ${PDS_DOMAIN} A ${IPV4}"
637
+
if [[ "$IPV6" != "Not available" ]]; then
638
+
echo " ${PDS_DOMAIN} AAAA ${IPV6}"
639
+
fi
640
+
echo " *.${PDS_DOMAIN} A ${IPV4}"
641
+
if [[ "$IPV6" != "Not available" ]]; then
642
+
echo " *.${PDS_DOMAIN} AAAA ${IPV6}"
643
+
fi
644
+
echo ""
645
+
echo -e "${GREEN}Enjoy your new AT Protocol PDS!${NC}"
+14
-4
src/api/identity/plc/submit.rs
+14
-4
src/api/identity/plc/submit.rs
···
218
218
}
219
219
}
220
220
221
-
if let Err(e) = sqlx::query!(
222
-
"INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity')",
221
+
match sqlx::query!(
222
+
"INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
223
223
did
224
224
)
225
-
.execute(&state.db)
225
+
.fetch_one(&state.db)
226
226
.await
227
227
{
228
-
warn!("Failed to sequence identity event: {:?}", e);
228
+
Ok(row) => {
229
+
if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq))
230
+
.execute(&state.db)
231
+
.await
232
+
{
233
+
warn!("Failed to notify identity event: {:?}", e);
234
+
}
235
+
}
236
+
Err(e) => {
237
+
warn!("Failed to sequence identity event: {:?}", e);
238
+
}
229
239
}
230
240
231
241
info!("Submitted PLC operation for user {}", did);
+7
-2
src/api/repo/import.rs
+7
-2
src/api/repo/import.rs
···
390
390
let blobs: Vec<String> = vec![];
391
391
let blocks_cids: Vec<String> = vec![];
392
392
393
-
sqlx::query!(
393
+
let seq_row = sqlx::query!(
394
394
r#"
395
395
INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)
396
396
VALUES ($1, 'commit', $2, $3, $4, $5, $6)
397
+
RETURNING seq
397
398
"#,
398
399
did,
399
400
commit_cid,
···
402
403
&blobs,
403
404
&blocks_cids
404
405
)
405
-
.execute(&state.db)
406
+
.fetch_one(&state.db)
406
407
.await?;
408
+
409
+
sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq_row.seq))
410
+
.execute(&state.db)
411
+
.await?;
407
412
408
413
Ok(())
409
414
}
+72
-19
src/api/server/session.rs
+72
-19
src/api/server/session.rs
···
176
176
State(state): State<AppState>,
177
177
BearerAuth(auth_user): BearerAuth,
178
178
) -> Response {
179
-
match sqlx::query!("SELECT handle, email FROM users WHERE did = $1", auth_user.did)
180
-
.fetch_optional(&state.db)
181
-
.await
179
+
match sqlx::query!(
180
+
r#"SELECT
181
+
handle, email, email_confirmed,
182
+
preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel",
183
+
discord_verified, telegram_verified, signal_verified
184
+
FROM users WHERE did = $1"#,
185
+
auth_user.did
186
+
)
187
+
.fetch_optional(&state.db)
188
+
.await
182
189
{
183
-
Ok(Some(row)) => Json(json!({
184
-
"handle": row.handle,
185
-
"did": auth_user.did,
186
-
"email": row.email,
187
-
"didDoc": {}
188
-
})).into_response(),
190
+
Ok(Some(row)) => {
191
+
let (preferred_channel, preferred_channel_verified) = match row.preferred_channel {
192
+
crate::notifications::NotificationChannel::Email => ("email", row.email_confirmed),
193
+
crate::notifications::NotificationChannel::Discord => ("discord", row.discord_verified),
194
+
crate::notifications::NotificationChannel::Telegram => ("telegram", row.telegram_verified),
195
+
crate::notifications::NotificationChannel::Signal => ("signal", row.signal_verified),
196
+
};
197
+
Json(json!({
198
+
"handle": row.handle,
199
+
"did": auth_user.did,
200
+
"email": row.email,
201
+
"emailConfirmed": row.email_confirmed,
202
+
"preferredChannel": preferred_channel,
203
+
"preferredChannelVerified": preferred_channel_verified,
204
+
"didDoc": {}
205
+
})).into_response()
206
+
}
189
207
Ok(None) => ApiError::AuthenticationFailed.into_response(),
190
208
Err(e) => {
191
209
error!("Database error in get_session: {:?}", e);
···
373
391
return ApiError::InternalError.into_response();
374
392
}
375
393
376
-
match sqlx::query!("SELECT handle FROM users WHERE did = $1", session_row.did)
377
-
.fetch_optional(&state.db)
378
-
.await
394
+
match sqlx::query!(
395
+
r#"SELECT
396
+
handle, email, email_confirmed,
397
+
preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel",
398
+
discord_verified, telegram_verified, signal_verified
399
+
FROM users WHERE did = $1"#,
400
+
session_row.did
401
+
)
402
+
.fetch_optional(&state.db)
403
+
.await
379
404
{
380
-
Ok(Some(u)) => Json(json!({
381
-
"accessJwt": new_access_meta.token,
382
-
"refreshJwt": new_refresh_meta.token,
383
-
"handle": u.handle,
384
-
"did": session_row.did
385
-
})).into_response(),
405
+
Ok(Some(u)) => {
406
+
let (preferred_channel, preferred_channel_verified) = match u.preferred_channel {
407
+
crate::notifications::NotificationChannel::Email => ("email", u.email_confirmed),
408
+
crate::notifications::NotificationChannel::Discord => ("discord", u.discord_verified),
409
+
crate::notifications::NotificationChannel::Telegram => ("telegram", u.telegram_verified),
410
+
crate::notifications::NotificationChannel::Signal => ("signal", u.signal_verified),
411
+
};
412
+
Json(json!({
413
+
"accessJwt": new_access_meta.token,
414
+
"refreshJwt": new_refresh_meta.token,
415
+
"handle": u.handle,
416
+
"did": session_row.did,
417
+
"email": u.email,
418
+
"emailConfirmed": u.email_confirmed,
419
+
"preferredChannel": preferred_channel,
420
+
"preferredChannelVerified": preferred_channel_verified
421
+
})).into_response()
422
+
}
386
423
Ok(None) => {
387
424
error!("User not found for existing session: {}", session_row.did);
388
425
ApiError::InternalError.into_response()
···
408
445
pub refresh_jwt: String,
409
446
pub handle: String,
410
447
pub did: String,
448
+
pub email: Option<String>,
449
+
pub email_confirmed: bool,
450
+
pub preferred_channel: String,
451
+
pub preferred_channel_verified: bool,
411
452
}
412
453
413
454
pub async fn confirm_signup(
···
418
459
419
460
let row = match sqlx::query!(
420
461
r#"SELECT
421
-
u.id, u.did, u.handle,
462
+
u.id, u.did, u.handle, u.email,
422
463
u.email_confirmation_code,
423
464
u.email_confirmation_code_expires_at,
424
465
u.preferred_notification_channel as "channel: crate::notifications::NotificationChannel",
···
527
568
warn!("Failed to enqueue welcome notification: {:?}", e);
528
569
}
529
570
571
+
let email_confirmed = matches!(row.channel, crate::notifications::NotificationChannel::Email);
572
+
let preferred_channel = match row.channel {
573
+
crate::notifications::NotificationChannel::Email => "email",
574
+
crate::notifications::NotificationChannel::Discord => "discord",
575
+
crate::notifications::NotificationChannel::Telegram => "telegram",
576
+
crate::notifications::NotificationChannel::Signal => "signal",
577
+
};
578
+
530
579
Json(ConfirmSignupOutput {
531
580
access_jwt: access_meta.token,
532
581
refresh_jwt: refresh_meta.token,
533
582
handle: row.handle,
534
583
did: row.did,
584
+
email: row.email,
585
+
email_confirmed,
586
+
preferred_channel: preferred_channel.to_string(),
587
+
preferred_channel_verified: true,
535
588
}).into_response()
536
589
}
537
590
+9
-1
src/sync/listener.rs
+9
-1
src/sync/listener.rs
···
57
57
loop {
58
58
let notification = listener.recv().await?;
59
59
let payload = notification.payload();
60
+
debug!(payload = %payload, "Received postgres notification");
60
61
61
62
let seq_id: i64 = match payload.parse() {
62
63
Ok(id) => id,
···
110
111
.await?;
111
112
112
113
if let Some(event) = event {
113
-
let _ = state.firehose_tx.send(event);
114
+
match state.firehose_tx.send(event) {
115
+
Ok(receiver_count) => {
116
+
debug!(seq = seq_id, receivers = receiver_count, "Broadcast event to firehose");
117
+
}
118
+
Err(e) => {
119
+
warn!(seq = seq_id, error = %e, "Failed to broadcast event (no receivers?)");
120
+
}
121
+
}
114
122
LAST_BROADCAST_SEQ.store(seq_id, Ordering::SeqCst);
115
123
} else {
116
124
warn!(seq = seq_id, "Received notification but could not find row in repo_seq");