DRY pass #2, batch 8, F17: a person and an account have one shape each (#5372)

Web: GroupList's Person was adapters/repo's Member field for field, and
AccountList's Account was the session store's User; both now import them.
The bridge's Identity stays, as the Tauri mirror of the core's struct.

Server: auth._serialize_user and accounts' _serialize_account wrote the same
four fields. serialize.serialize_user is serialize_person plus is_admin, and
the account list adds created_at to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 15:11:29 -04:00
co-authored by Claude Opus 5.5
parent 0d82e26eea
commit 8c45725a2a
5 changed files with 25 additions and 43 deletions
+4 -11
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { api } from "../api/client";
import { useSessionStore } from "../stores/session";
import { useSessionStore, type User } from "../stores/session";
import { appLink } from "../router/links";
import OneTimeLink from "./OneTimeLink.vue";
import { useRowAction, useLoad } from "../composables/useAction";
@@ -10,24 +10,17 @@ import { useRowAction, useLoad } from "../composables/useAction";
// (#5173). The admin hands the link over; a person can also mail one to themselves
// when the server can send email (#5266).
interface Account {
id: string;
email: string;
display_name: string;
is_admin: boolean;
}
const session = useSessionStore();
const accounts = ref<Account[]>([]);
const accounts = ref<User[]>([]);
const { loading, error, load } = useLoad(async () => {
accounts.value = (await api.get<{ accounts: Account[] }>("/api/accounts")).accounts;
accounts.value = (await api.get<{ accounts: User[] }>("/api/accounts")).accounts;
}, "Couldn't load accounts.");
const { busy, act } = useRowAction();
// The reset link just made, and whose it is. Never retrievable again once dismissed.
const fresh = ref<{ link: string; email: string } | null>(null);
async function resetLink(account: Account) {
async function resetLink(account: User) {
const self = account.id === session.user?.id;
const warning = self
? "Make a password reset link for your own account? Using it signs you out everywhere, this browser included."
+6 -11
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { api } from "../api/client";
import type { Member } from "../adapters/repo";
import { errorMessage } from "../api/errors";
import BaseButton from "./BaseButton.vue";
import BaseInput from "./BaseInput.vue";
@@ -11,24 +12,18 @@ import { useRowAction, useLoad } from "../composables/useAction";
// the instance's; anyone can share with it, and a note shared with it reaches whoever
// is in it at the time, so adding or removing someone here changes what they see.
interface Person {
id: string;
display_name: string;
email: string;
}
interface Group {
id: string;
name: string;
members: Person[];
members: Member[];
}
const groups = ref<Group[]>([]);
const accounts = ref<Person[]>([]);
const accounts = ref<Member[]>([]);
const { loading, error, load } = useLoad(async () => {
const [g, a] = await Promise.all([
api.get<{ groups: Group[] }>("/api/groups"),
api.get<{ accounts: Person[] }>("/api/accounts"),
api.get<{ accounts: Member[] }>("/api/accounts"),
]);
groups.value = g.groups;
accounts.value = a.accounts;
@@ -41,7 +36,7 @@ const picks = ref<Record<string, string>>({});
const byName = (a: Group, b: Group) => a.name.localeCompare(b.name);
function outside(group: Group): Person[] {
function outside(group: Group): Member[] {
const inside = new Set(group.members.map((m) => m.id));
return accounts.value.filter((a) => !inside.has(a.id));
}
@@ -102,7 +97,7 @@ function addMember(group: Group) {
);
}
function removeMember(group: Group, person: Person) {
function removeMember(group: Group, person: Member) {
void act(
group.id,
async () => replace(await api.del<Group>(`/api/groups/${group.id}/members/${person.id}`)),