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:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { api } from "../api/client";
|
||||||
import { useSessionStore } from "../stores/session";
|
import { useSessionStore, type User } from "../stores/session";
|
||||||
import { appLink } from "../router/links";
|
import { appLink } from "../router/links";
|
||||||
import OneTimeLink from "./OneTimeLink.vue";
|
import OneTimeLink from "./OneTimeLink.vue";
|
||||||
import { useRowAction, useLoad } from "../composables/useAction";
|
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
|
// (#5173). The admin hands the link over; a person can also mail one to themselves
|
||||||
// when the server can send email (#5266).
|
// when the server can send email (#5266).
|
||||||
|
|
||||||
interface Account {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
display_name: string;
|
|
||||||
is_admin: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = useSessionStore();
|
const session = useSessionStore();
|
||||||
|
|
||||||
const accounts = ref<Account[]>([]);
|
const accounts = ref<User[]>([]);
|
||||||
const { loading, error, load } = useLoad(async () => {
|
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.");
|
}, "Couldn't load accounts.");
|
||||||
const { busy, act } = useRowAction();
|
const { busy, act } = useRowAction();
|
||||||
// The reset link just made, and whose it is. Never retrievable again once dismissed.
|
// The reset link just made, and whose it is. Never retrievable again once dismissed.
|
||||||
const fresh = ref<{ link: string; email: string } | null>(null);
|
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 self = account.id === session.user?.id;
|
||||||
const warning = self
|
const warning = self
|
||||||
? "Make a password reset link for your own account? Using it signs you out everywhere, this browser included."
|
? "Make a password reset link for your own account? Using it signs you out everywhere, this browser included."
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { api } from "../api/client";
|
||||||
|
import type { Member } from "../adapters/repo";
|
||||||
import { errorMessage } from "../api/errors";
|
import { errorMessage } from "../api/errors";
|
||||||
import BaseButton from "./BaseButton.vue";
|
import BaseButton from "./BaseButton.vue";
|
||||||
import BaseInput from "./BaseInput.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
|
// 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.
|
// 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 {
|
interface Group {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
members: Person[];
|
members: Member[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = ref<Group[]>([]);
|
const groups = ref<Group[]>([]);
|
||||||
const accounts = ref<Person[]>([]);
|
const accounts = ref<Member[]>([]);
|
||||||
const { loading, error, load } = useLoad(async () => {
|
const { loading, error, load } = useLoad(async () => {
|
||||||
const [g, a] = await Promise.all([
|
const [g, a] = await Promise.all([
|
||||||
api.get<{ groups: Group[] }>("/api/groups"),
|
api.get<{ groups: Group[] }>("/api/groups"),
|
||||||
api.get<{ accounts: Person[] }>("/api/accounts"),
|
api.get<{ accounts: Member[] }>("/api/accounts"),
|
||||||
]);
|
]);
|
||||||
groups.value = g.groups;
|
groups.value = g.groups;
|
||||||
accounts.value = a.accounts;
|
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);
|
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));
|
const inside = new Set(group.members.map((m) => m.id));
|
||||||
return accounts.value.filter((a) => !inside.has(a.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(
|
void act(
|
||||||
group.id,
|
group.id,
|
||||||
async () => replace(await api.del<Group>(`/api/groups/${group.id}/members/${person.id}`)),
|
async () => replace(await api.del<Group>(`/api/groups/${group.id}/members/${person.id}`)),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from .models.user import User
|
|||||||
from .password_resets import issue
|
from .password_resets import issue
|
||||||
from .proxy import client_address
|
from .proxy import client_address
|
||||||
from .responses import not_found, parse_uuid
|
from .responses import not_found, parse_uuid
|
||||||
|
from .serialize import serialize_user
|
||||||
|
|
||||||
bp = Blueprint("accounts", __name__, url_prefix="/api/accounts")
|
bp = Blueprint("accounts", __name__, url_prefix="/api/accounts")
|
||||||
|
|
||||||
@@ -26,13 +27,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _serialize_account(user: User) -> dict:
|
def _serialize_account(user: User) -> dict:
|
||||||
return {
|
return {**serialize_user(user), "created_at": iso(user.created_at)}
|
||||||
"id": str(user.id),
|
|
||||||
"email": user.email,
|
|
||||||
"display_name": user.display_name,
|
|
||||||
"is_admin": user.is_admin,
|
|
||||||
"created_at": iso(user.created_at),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("")
|
@bp.get("")
|
||||||
|
|||||||
+6
-14
@@ -19,6 +19,7 @@ from .models.device_token import DeviceToken
|
|||||||
from .models.user import User
|
from .models.user import User
|
||||||
from .proxy import client_address
|
from .proxy import client_address
|
||||||
from .responses import json_error, not_found, parse_uuid, too_many
|
from .responses import json_error, not_found, parse_uuid, too_many
|
||||||
|
from .serialize import serialize_user
|
||||||
from .ratelimit import (
|
from .ratelimit import (
|
||||||
register_by_address,
|
register_by_address,
|
||||||
reset_mail_by_account,
|
reset_mail_by_account,
|
||||||
@@ -63,15 +64,6 @@ SETUP_CLOSED = (
|
|||||||
DEVICE_NAME_CAP = 100
|
DEVICE_NAME_CAP = 100
|
||||||
|
|
||||||
|
|
||||||
def _serialize_user(user: User) -> dict:
|
|
||||||
return {
|
|
||||||
"id": str(user.id),
|
|
||||||
"email": user.email,
|
|
||||||
"display_name": user.display_name,
|
|
||||||
"is_admin": user.is_admin,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _sign_in(user: User) -> None:
|
def _sign_in(user: User) -> None:
|
||||||
session[SESSION_KEY] = str(user.id)
|
session[SESSION_KEY] = str(user.id)
|
||||||
session[EPOCH_KEY] = user.session_epoch
|
session[EPOCH_KEY] = user.session_epoch
|
||||||
@@ -313,7 +305,7 @@ async def register():
|
|||||||
)
|
)
|
||||||
detail = "first account, admin" if is_first else ("by invite" if invite_id else None)
|
detail = "first account, admin" if is_first else ("by invite" if invite_id else None)
|
||||||
await audit.record(audit.REGISTERED, user_id=user.id, email=email, detail=detail)
|
await audit.record(audit.REGISTERED, user_id=user.id, email=email, detail=detail)
|
||||||
return jsonify(_serialize_user(user)), 201
|
return jsonify(serialize_user(user)), 201
|
||||||
|
|
||||||
|
|
||||||
async def _check_credentials(db, email: str, password: str, route: str) -> User | None:
|
async def _check_credentials(db, email: str, password: str, route: str) -> User | None:
|
||||||
@@ -362,7 +354,7 @@ async def login():
|
|||||||
_sign_in(user)
|
_sign_in(user)
|
||||||
logger.info("sign-in ok email=%s from=%s", email, client_address())
|
logger.info("sign-in ok email=%s from=%s", email, client_address())
|
||||||
await audit.record(audit.SIGN_IN, user_id=user.id, email=email)
|
await audit.record(audit.SIGN_IN, user_id=user.id, email=email)
|
||||||
return jsonify(_serialize_user(user))
|
return jsonify(serialize_user(user))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/logout")
|
@bp.post("/logout")
|
||||||
@@ -379,7 +371,7 @@ async def me():
|
|||||||
if user is None:
|
if user is None:
|
||||||
_sign_out()
|
_sign_out()
|
||||||
return _unauthenticated()
|
return _unauthenticated()
|
||||||
return jsonify(_serialize_user(user))
|
return jsonify(serialize_user(user))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/storage")
|
@bp.get("/storage")
|
||||||
@@ -469,7 +461,7 @@ async def reset_password():
|
|||||||
"password reset email=%s devices_unlinked=%s from=%s", user.email, unlinked, client_address()
|
"password reset email=%s devices_unlinked=%s from=%s", user.email, unlinked, client_address()
|
||||||
)
|
)
|
||||||
await audit.record(audit.PASSWORD_RESET, user_id=user.id, email=user.email, detail=_unlinked(unlinked))
|
await audit.record(audit.PASSWORD_RESET, user_id=user.id, email=user.email, detail=_unlinked(unlinked))
|
||||||
return jsonify(_serialize_user(user))
|
return jsonify(serialize_user(user))
|
||||||
|
|
||||||
|
|
||||||
async def _sign_out_elsewhere(db, user: User, keep_token: str | None) -> int:
|
async def _sign_out_elsewhere(db, user: User, keep_token: str | None) -> int:
|
||||||
@@ -619,7 +611,7 @@ async def device_login():
|
|||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await audit.record(audit.DEVICE_LINKED, user_id=user.id, email=email, detail=row.name)
|
await audit.record(audit.DEVICE_LINKED, user_id=user.id, email=email, detail=row.name)
|
||||||
return jsonify({"token": token, "device": _serialize_device(row), "user": _serialize_user(user)}), 201
|
return jsonify({"token": token, "device": _serialize_device(row), "user": serialize_user(user)}), 201
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/devices")
|
@bp.post("/devices")
|
||||||
|
|||||||
@@ -32,3 +32,10 @@ def serialize_person(user: User) -> dict:
|
|||||||
choosing them takes and all a fellow member needs to know. The share directory,
|
choosing them takes and all a fellow member needs to know. The share directory,
|
||||||
a share's recipient and a group's members all show people this way."""
|
a share's recipient and a group's members all show people this way."""
|
||||||
return {"id": str(user.id), "display_name": user.display_name, "email": user.email}
|
return {"id": str(user.id), "display_name": user.display_name, "email": user.email}
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_user(user: User) -> dict:
|
||||||
|
"""An account as its owner sees it: the person plus whether they are an admin.
|
||||||
|
Sign-in, /me and device login answer with this; the admin's account list adds
|
||||||
|
when each was made."""
|
||||||
|
return {**serialize_person(user), "is_admin": user.is_admin}
|
||||||
|
|||||||
Reference in New Issue
Block a user