Skip to content

Commit

Permalink
chore: add database migration script for latest username update.
Browse files Browse the repository at this point in the history
  • Loading branch information
zicklag committed Nov 16, 2024
1 parent 508f757 commit a716631
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 111 deletions.
4 changes: 2 additions & 2 deletions .env.local
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BACKEND_URL="http://127.0.0.1:7431"
BACKEND_SECRET="temporarydevelopmentkey"
INSTANCE_SUBSPACE_SECRET=afkknpungyscs5gszd4ywzaq4jghwt2k7e2liejajaqjjps2pqsq
INSTANCE_SUBSPACE_SECRET=gqvkucapoouch6xhhnn3pnqu7zpwpd3a4nctz2vkm2qrsnfbf6ha
RAUTHY_URL="http://localhost:8921"
SMTP_HOST="localhost"
SMTP_PORT="2525"
Expand All @@ -22,7 +22,7 @@ FEEDBACK_WEBHOOK=""
PUBLIC_URL=http://localhost:9523
PUBLIC_DOMAIN=localhost:9523
DNS_PORT=7753
PUBLIC_USER_DOMAIN_PARENT=user.localhost:9523
PUBLIC_USER_DOMAIN_PARENT=weird.one
REDIS_URL="redis://localhost:7634"

# DNS
Expand Down
12 changes: 7 additions & 5 deletions leaf/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,14 +666,16 @@ export class RpcClient {

async add_components<C extends Component>(
link: ExactLink,
components: C[],
components: (C | { schema: Digest; data: Uint8Array })[],
replaceExisting = true
): Promise<Digest> {
let componentData = components.map((component) => {
return {
schema: Object.getPrototypeOf(component).constructor.schemaId(),
data: component.serialize()
};
return component instanceof Component
? {
schema: Object.getPrototypeOf(component).constructor.schemaId(),
data: component.serialize()
}
: component;
});
const resp = await this.#send_req({
AddComponents: {
Expand Down
36 changes: 19 additions & 17 deletions src/lib/usernames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function setUserSubspace(rauthyId: string, subspace: SubspaceId) {
}

export async function claimUsername(
input: { username: string } | { domain: string },
input: { username: string } | { domain: string; skipDomainCheck?: boolean },
rauthyId: string
) {
const rauthyIdKey = USER_RAUTHY_IDS_PREFIX + rauthyId;
Expand All @@ -31,36 +31,38 @@ export async function claimUsername(
// Claiming a local username

if (!input.username.match(validUsernameRegex)) {
throw 'Username does not pass valid username check.';
throw `Username does not pass valid username check: '${input.username}'`;
} else {
username = input.username + '.' + env.PUBLIC_USER_DOMAIN_PARENT;
}
} else {
// Claim a custom domain
const isApex = input.domain.split('.').length == 2;

if (isApex) {
const ips = await resolveAuthoritative(input.domain, 'A');
let matches = 0;
for (const ip of ips) {
if (ip in APP_IPS) {
matches += 1;
} else {
throw `DNS validation failed: ${input.domain} resolves to \
if (!input.skipDomainCheck) {
if (isApex) {
const ips = await resolveAuthoritative(input.domain, 'A');
let matches = 0;
for (const ip of ips) {
if (ip in APP_IPS) {
matches += 1;
} else {
throw `DNS validation failed: ${input.domain} resolves to \
an IP address ${ip} that is not the Weird server.`;
}
}
}

if (matches == 0) {
throw `DNS validation failed: ${input.domain} does not resolve \
if (matches == 0) {
throw `DNS validation failed: ${input.domain} does not resolve \
to the weird server.`;
}
} else {
}
} else {
}

throw "Can't use custom domains yet";
throw "Can't use custom domains yet";
}

// username = input.domain;
username = input.domain;
}

const usernameKey = USER_NAMES_PREFIX + username;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(internal)/__internal__/admin/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
<li><a href="/__internal__/admin/dns/resolve">DNS Resolver</a></li>
<li><a href="/__internal__/admin/dns/set">DNS Record Set</a></li>
<li><a href="/__internal__/admin/usernames">Username Manager</a></li>
<li><a href="/__internal__/admin/migration">Migration ( Temp )</a></li>
<li><a href="/__internal__/admin/migration-508f757">Migration ( 508f757 )</a></li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { claimUsername, unsetUsername, listUsers, userSubspaceByRauthyId } from '$lib/usernames';
import { fail, type ServerLoad } from '@sveltejs/kit';
import type { Actions } from './$types';
import {
DatabaseDumpSchema,
base32Decode,
borshDeserialize,
type DatabaseDump,
type SubspaceId,
type DatabaseDumpDocument,
type DatabaseDumpSubspace,
formatEntityPath,
type ExactLink,
Component,
BorshSchema
} from 'leaf-proto';
import { WEIRD_NAMESPACE, leafClient, subspace_link } from '$lib/leaf';
import _ from 'underscore';
import { CommonMark, RawImage } from 'leaf-proto/components';
import { WeirdCustomDomain, setAvatar } from '$lib/leaf/profile';

import { createAvatar } from '@dicebear/core';
import { glass } from '@dicebear/collection';

export const load: ServerLoad = async ({}) => {
const users = [];
for await (const user of listUsers()) users.push(user);
return { users };
};

class Username extends Component {
value: string = '';
constructor(s: string) {
super();
this.value = s;
}
static componentName(): string {
return 'Username';
}
static borshSchema(): BorshSchema {
return BorshSchema.String;
}
static specification(): Component[] {
return [new CommonMark('The username of the user represented by this entity.')];
}
}

export const actions = {
migrate: async ({ request }) => {
const formData = await request.formData();
const subspaceIdStr = formData.get('subspaceId');
if (!subspaceIdStr)
return fail(400, {
error: 'You must provide the weird instance subspace ID to import from.'
});
let subspaceId: SubspaceId;
try {
subspaceId = base32Decode(subspaceIdStr.toString());
} catch (_) {
return fail(400, { error: 'Could not parse subspace ID.' });
}
const dumpFormData = formData.get('dump');
if (!dumpFormData) return fail(400, { error: 'You must provide database dump file.' });
const dumpData = new Uint8Array(await (dumpFormData as File).arrayBuffer());
const dump: DatabaseDump = borshDeserialize(DatabaseDumpSchema, dumpData);

let doc: DatabaseDumpDocument | undefined;
if (dump.documents.size != 1) return fail(400, { error: 'Dump has multiple namespaces' });
for (const [namespace, document] of dump.documents) {
const n = new Uint8Array(namespace);
if (_.isEqual(n, WEIRD_NAMESPACE)) {
doc = document;
break;
}
}
if (doc === undefined) {
return fail(400, { error: 'Dump is missing weird namespace' });
}

let subspace: DatabaseDumpSubspace | undefined;
for (const [id, ss] of doc.subspaces) {
const i = new Uint8Array(id);
if (_.isEqual(subspaceId, i)) {
subspace = ss;
break;
}
}
if (subspace === undefined) {
return fail(400, { error: 'could not find specified subspace in dump' });
}

for (const [path, entity] of subspace) {
if (_.isEqual(path[0], { String: 'profiles' })) {
if ('String' in path[1]) {
const rauthyId = path[1].String;
const subspace = await userSubspaceByRauthyId(rauthyId);

// User profile
let newLink: ExactLink;
const isProfile = path.length == 2;
if (isProfile) {
newLink = subspace_link(subspace, null);
} else {
newLink = subspace_link(subspace, ...path.slice(2));
}

const components = [];
let username: string | undefined;
let customDomain: string | undefined;
let hasAvatar = false;
for (const [schema, componentDatas] of entity.components) {
const s = new Uint8Array(schema);
if (_.isEqual(Username.schemaId(), s)) {
username = Username.deserialize(new Uint8Array(componentDatas[0])) as any;
username = username?.split('@')[0].toLowerCase().replace('.', '-');
continue;
}
if (_.isEqual(WeirdCustomDomain.schemaId(), s)) {
customDomain = WeirdCustomDomain.deserialize(
new Uint8Array(componentDatas[0])
) as any;
continue;
}
if (_.isEqual(s, RawImage.schemaId())) {
hasAvatar = true;
}
for (const data of componentDatas) {
console;
components.push({ schema: new Uint8Array(schema), data: new Uint8Array(data) });
}
}
if (customDomain) {
await claimUsername({ domain: customDomain, skipDomainCheck: true }, rauthyId);
} else if (username) {
await claimUsername({ username }, rauthyId);
}
await leafClient.add_components(newLink, components);

if (isProfile && !hasAvatar) {
const avatar = createAvatar(glass, { seed: rauthyId, radius: 50 });
await setAvatar(
newLink,
new RawImage('image/svg+xml', new TextEncoder().encode(avatar.toString()))
);
}
}
}
}

return { dump: `${JSON.stringify(dump, null, ' ')}` };
}
} satisfies Actions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts">
import { base32Encode } from 'leaf-proto';
import type { ActionData, PageData } from './$types';
const { form, data }: { form: ActionData; data: PageData } = $props();
</script>

{#if form?.error}
<article class="pico-background-red-550">
{form.error}
</article>
{:else if form?.dump}
<article>
<pre>

{form.dump}
</pre>
</article>
{/if}

<h2>Migrate Database</h2>

<p>
Upload a dump of the previous version of the database and it will be imported and migrated to the
new version.
</p>

<p>
Below you can see the commits for the previous version of Weird that we are migrating from, and
the commit after that which includes the newer data model that we are migrating to.
</p>

<table>
<thead>
<tr>
<td>Previous Version Commit</td>
<td>Current Version Commit</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="https://github.com/muni-town/weird/commit/e291612d36be303eecbe136967c39fa5e2b6af5a"
>e291612d36be303eecbe136967c39fa5e2b6af5a</a
>
</td>
<td>
<a
href="https://github.com/muni-town/weird/commit/508f757686d3ca20fa5b8b4a29b09c2fe992b4a3"
>
508f757686d3ca20fa5b8b4a29b09c2fe992b4a3
</a>
</td>
</tr>
</tbody>
</table>

<form method="post" action="?/migrate" enctype="multipart/form-data">
<!-- svelte-ignore a11y_no_redundant_roles -->
<input name="subspaceId" placeholder="subspaceId" />
<input type="file" name="dump" accept=".bin" />
<button>Migrate</button>
</form>
49 changes: 0 additions & 49 deletions src/routes/(internal)/__internal__/admin/migration/+page.server.ts

This file was deleted.

Loading

0 comments on commit a716631

Please sign in to comment.