mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-02-05 18:58:08 -05:00
0422593ec6
Users are often puzzled why there are short recordings. Previously the only way to see this was to examine Moonfire's logs. This should be a much better experience to find it right in the UI where you're wondering, and without the potential the logs are gone. Fixes #302
653 lines
18 KiB
Rust
653 lines
18 KiB
Rust
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
|
|
|
//! JSON/TOML-compatible serde types for use in the web API and `moonfire-nvr.toml`.
|
|
|
|
use base::time::{Duration, Time};
|
|
use base::{err, Error};
|
|
use db::auth::SessionHash;
|
|
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
use std::ops::Not;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TopLevel<'a> {
|
|
pub time_zone_name: &'a str,
|
|
|
|
pub server_version: &'static str,
|
|
|
|
// Use a custom serializer which presents the map's values as a sequence and includes the
|
|
// "days" and "camera_configs" attributes or not, according to the respective bools.
|
|
#[serde(serialize_with = "TopLevel::serialize_cameras")]
|
|
pub cameras: (&'a db::LockedDatabase, bool, bool),
|
|
|
|
pub permissions: Permissions,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub user: Option<ToplevelUser>,
|
|
|
|
#[serde(serialize_with = "TopLevel::serialize_signals")]
|
|
pub signals: (&'a db::LockedDatabase, bool),
|
|
|
|
#[serde(serialize_with = "TopLevel::serialize_signal_types")]
|
|
pub signal_types: &'a db::LockedDatabase,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Session {
|
|
#[serde(serialize_with = "Session::serialize_csrf")]
|
|
pub csrf: SessionHash,
|
|
}
|
|
|
|
impl Session {
|
|
fn serialize_csrf<S>(csrf: &SessionHash, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut tmp = [0u8; 32];
|
|
csrf.encode_base64(&mut tmp);
|
|
serializer.serialize_str(::std::str::from_utf8(&tmp[..]).expect("base64 is UTF-8"))
|
|
}
|
|
}
|
|
|
|
/// JSON serialization wrapper for a single camera when processing `/api/` and
|
|
/// `/api/cameras/<uuid>/`. See `ref/api.md` for details.
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Camera<'a> {
|
|
pub uuid: Uuid,
|
|
pub id: i32,
|
|
pub short_name: &'a str,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub config: Option<&'a db::json::CameraConfig>,
|
|
|
|
#[serde(serialize_with = "Camera::serialize_streams")]
|
|
pub streams: [Option<Stream<'a>>; db::db::NUM_STREAM_TYPES],
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Stream<'a> {
|
|
pub id: i32,
|
|
pub retain_bytes: i64,
|
|
pub min_start_time_90k: Option<Time>,
|
|
pub max_end_time_90k: Option<Time>,
|
|
pub total_duration_90k: Duration,
|
|
pub total_sample_file_bytes: i64,
|
|
pub fs_bytes: i64,
|
|
pub record: bool,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[serde(serialize_with = "Stream::serialize_days")]
|
|
pub days: Option<db::days::Map<db::days::StreamValue>>,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub config: Option<&'a db::json::StreamConfig>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Signal<'a> {
|
|
pub id: u32,
|
|
#[serde(serialize_with = "Signal::serialize_cameras")]
|
|
pub cameras: (&'a db::Signal, &'a db::LockedDatabase),
|
|
pub uuid: Uuid,
|
|
pub type_: Uuid,
|
|
pub short_name: &'a str,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[serde(serialize_with = "Signal::serialize_days")]
|
|
pub days: Option<&'a db::days::Map<db::days::SignalValue>>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(tag = "base", content = "rel90k", rename_all = "camelCase")]
|
|
pub enum PostSignalsTimeBase {
|
|
Epoch(Time),
|
|
Now(Duration),
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LoginRequest<'a> {
|
|
pub username: &'a str,
|
|
pub password: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LogoutRequest<'a> {
|
|
#[serde(borrow)]
|
|
pub csrf: &'a str,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PostSignalsRequest<'a> {
|
|
#[serde(borrow)]
|
|
pub csrf: Option<&'a str>,
|
|
pub signal_ids: Vec<u32>,
|
|
pub states: Vec<u16>,
|
|
pub start: PostSignalsTimeBase,
|
|
pub end: PostSignalsTimeBase,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PostSignalsResponse {
|
|
pub time_90k: Time,
|
|
}
|
|
|
|
#[derive(Default, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Signals {
|
|
pub times_90k: Vec<Time>,
|
|
pub signal_ids: Vec<u32>,
|
|
pub states: Vec<u16>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SignalType<'a> {
|
|
pub uuid: Uuid,
|
|
|
|
#[serde(serialize_with = "SignalType::serialize_states")]
|
|
pub states: &'a db::signal::Type,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SignalTypeState<'a> {
|
|
value: u8,
|
|
name: &'a str,
|
|
|
|
#[serde(skip_serializing_if = "Not::not")]
|
|
motion: bool,
|
|
color: &'a str,
|
|
}
|
|
|
|
impl<'a> Camera<'a> {
|
|
pub fn wrap(
|
|
c: &'a db::Camera,
|
|
db: &'a db::LockedDatabase,
|
|
include_days: bool,
|
|
include_config: bool,
|
|
) -> Result<Self, Error> {
|
|
Ok(Camera {
|
|
uuid: c.uuid,
|
|
id: c.id,
|
|
short_name: &c.short_name,
|
|
config: match include_config {
|
|
false => None,
|
|
true => Some(&c.config),
|
|
},
|
|
streams: [
|
|
Stream::wrap(db, c.streams[0], include_days, include_config)?,
|
|
Stream::wrap(db, c.streams[1], include_days, include_config)?,
|
|
Stream::wrap(db, c.streams[2], include_days, include_config)?,
|
|
],
|
|
})
|
|
}
|
|
|
|
fn serialize_streams<S>(
|
|
streams: &[Option<Stream>; db::db::NUM_STREAM_TYPES],
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut map = serializer.serialize_map(Some(streams.len()))?;
|
|
for (i, s) in streams.iter().enumerate() {
|
|
if let Some(ref s) = *s {
|
|
map.serialize_key(
|
|
db::StreamType::from_index(i)
|
|
.expect("invalid stream type index")
|
|
.as_str(),
|
|
)?;
|
|
map.serialize_value(s)?;
|
|
}
|
|
}
|
|
map.end()
|
|
}
|
|
}
|
|
|
|
impl<'a> Stream<'a> {
|
|
fn wrap(
|
|
db: &'a db::LockedDatabase,
|
|
id: Option<i32>,
|
|
include_days: bool,
|
|
include_config: bool,
|
|
) -> Result<Option<Self>, Error> {
|
|
let id = match id {
|
|
Some(id) => id,
|
|
None => return Ok(None),
|
|
};
|
|
let s = db
|
|
.streams_by_id()
|
|
.get(&id)
|
|
.ok_or_else(|| err!(Internal, msg("missing stream {id}")))?;
|
|
Ok(Some(Stream {
|
|
id: s.id,
|
|
retain_bytes: s.config.retain_bytes,
|
|
min_start_time_90k: s.range.as_ref().map(|r| r.start),
|
|
max_end_time_90k: s.range.as_ref().map(|r| r.end),
|
|
total_duration_90k: s.duration,
|
|
total_sample_file_bytes: s.sample_file_bytes,
|
|
fs_bytes: s.fs_bytes,
|
|
record: s.config.mode == db::json::STREAM_MODE_RECORD,
|
|
days: if include_days { Some(s.days()) } else { None },
|
|
config: match include_config {
|
|
false => None,
|
|
true => Some(&s.config),
|
|
},
|
|
}))
|
|
}
|
|
|
|
fn serialize_days<S>(
|
|
days: &Option<db::days::Map<db::days::StreamValue>>,
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let days = match days.as_ref() {
|
|
Some(d) => d,
|
|
None => return serializer.serialize_none(),
|
|
};
|
|
let mut map = serializer.serialize_map(Some(days.len()))?;
|
|
for (k, v) in days {
|
|
map.serialize_key(k.as_ref())?;
|
|
let bounds = k.bounds();
|
|
map.serialize_value(&StreamDayValue {
|
|
start_time_90k: bounds.start,
|
|
end_time_90k: bounds.end,
|
|
total_duration_90k: v.duration,
|
|
})?;
|
|
}
|
|
map.end()
|
|
}
|
|
}
|
|
|
|
impl<'a> Signal<'a> {
|
|
pub fn wrap(s: &'a db::Signal, db: &'a db::LockedDatabase, include_days: bool) -> Self {
|
|
Signal {
|
|
id: s.id,
|
|
cameras: (s, db),
|
|
uuid: s.uuid,
|
|
type_: s.type_,
|
|
short_name: &s.config.short_name,
|
|
days: if include_days { Some(&s.days) } else { None },
|
|
}
|
|
}
|
|
|
|
fn serialize_cameras<S>(
|
|
cameras: &(&db::Signal, &db::LockedDatabase),
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let (s, db) = cameras;
|
|
let mut map = serializer.serialize_map(Some(s.config.camera_associations.len()))?;
|
|
for (camera_id, association) in &s.config.camera_associations {
|
|
let c = db.cameras_by_id().get(camera_id).ok_or_else(|| {
|
|
S::Error::custom(format!("signal has missing camera id {camera_id}"))
|
|
})?;
|
|
map.serialize_key(&c.uuid)?;
|
|
map.serialize_value(association.as_str())?;
|
|
}
|
|
map.end()
|
|
}
|
|
|
|
fn serialize_days<S>(
|
|
days: &Option<&db::days::Map<db::days::SignalValue>>,
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let days = match *days {
|
|
Some(d) => d,
|
|
None => return serializer.serialize_none(),
|
|
};
|
|
let mut map = serializer.serialize_map(Some(days.len()))?;
|
|
for (k, v) in days {
|
|
map.serialize_key(k.as_ref())?;
|
|
let bounds = k.bounds();
|
|
map.serialize_value(&SignalDayValue {
|
|
start_time_90k: bounds.start,
|
|
end_time_90k: bounds.end,
|
|
states: &v.states[..],
|
|
})?;
|
|
}
|
|
map.end()
|
|
}
|
|
}
|
|
|
|
impl<'a> SignalType<'a> {
|
|
pub fn wrap(uuid: Uuid, type_: &'a db::signal::Type) -> Self {
|
|
SignalType {
|
|
uuid,
|
|
states: type_,
|
|
}
|
|
}
|
|
|
|
fn serialize_states<S>(type_: &db::signal::Type, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut seq = serializer.serialize_seq(Some(type_.config.values.len()))?;
|
|
for (&value, config) in &type_.config.values {
|
|
seq.serialize_element(&SignalTypeState::wrap(value, config))?;
|
|
}
|
|
seq.end()
|
|
}
|
|
}
|
|
|
|
impl<'a> SignalTypeState<'a> {
|
|
pub fn wrap(value: u8, config: &'a db::json::SignalTypeValueConfig) -> Self {
|
|
SignalTypeState {
|
|
value,
|
|
name: &config.name,
|
|
motion: config.motion,
|
|
color: &config.color,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct StreamDayValue {
|
|
pub start_time_90k: Time,
|
|
pub end_time_90k: Time,
|
|
pub total_duration_90k: Duration,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct SignalDayValue<'a> {
|
|
pub start_time_90k: Time,
|
|
pub end_time_90k: Time,
|
|
pub states: &'a [u64],
|
|
}
|
|
|
|
impl<'a> TopLevel<'a> {
|
|
/// Serializes cameras as a list (rather than a map), optionally including the `days` and
|
|
/// `cameras` fields.
|
|
fn serialize_cameras<S>(
|
|
cameras: &(&db::LockedDatabase, bool, bool),
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let (db, include_days, include_config) = *cameras;
|
|
let cs = db.cameras_by_id();
|
|
let mut seq = serializer.serialize_seq(Some(cs.len()))?;
|
|
for c in cs.values() {
|
|
seq.serialize_element(
|
|
&Camera::wrap(c, db, include_days, include_config).map_err(S::Error::custom)?,
|
|
)?;
|
|
}
|
|
seq.end()
|
|
}
|
|
|
|
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
|
|
fn serialize_signals<S>(
|
|
signals: &(&db::LockedDatabase, bool),
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let (db, include_days) = *signals;
|
|
let ss = db.signals_by_id();
|
|
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
|
|
for s in ss.values() {
|
|
seq.serialize_element(&Signal::wrap(s, db, include_days))?;
|
|
}
|
|
seq.end()
|
|
}
|
|
|
|
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
|
|
fn serialize_signal_types<S>(db: &db::LockedDatabase, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let ss = db.signal_types_by_uuid();
|
|
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
|
|
for (u, t) in ss {
|
|
seq.serialize_element(&SignalType::wrap(*u, t))?;
|
|
}
|
|
seq.end()
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ListRecordings<'a> {
|
|
pub recordings: Vec<Recording>,
|
|
|
|
// There are likely very few video sample entries for a given stream in a given day, so
|
|
// representing with an unordered Vec (and having O(n) insert-if-absent) is probably better
|
|
// than dealing with a HashSet's code bloat.
|
|
#[serde(serialize_with = "ListRecordings::serialize_video_sample_entries")]
|
|
pub video_sample_entries: (&'a db::LockedDatabase, Vec<i32>),
|
|
}
|
|
|
|
impl<'a> ListRecordings<'a> {
|
|
fn serialize_video_sample_entries<S>(
|
|
video_sample_entries: &(&db::LockedDatabase, Vec<i32>),
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let (db, ref v) = *video_sample_entries;
|
|
let mut map = serializer.serialize_map(Some(v.len()))?;
|
|
for id in v {
|
|
map.serialize_entry(
|
|
id,
|
|
&VideoSampleEntry::from(db.video_sample_entries_by_id().get(id).unwrap()),
|
|
)?;
|
|
}
|
|
map.end()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Recording {
|
|
pub start_time_90k: i64,
|
|
pub end_time_90k: i64,
|
|
pub sample_file_bytes: i64,
|
|
pub video_samples: i64,
|
|
pub video_sample_entry_id: i32,
|
|
pub start_id: i32,
|
|
pub open_id: u32,
|
|
pub run_start_id: i32,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub first_uncommitted: Option<i32>,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub end_id: Option<i32>,
|
|
|
|
#[serde(skip_serializing_if = "Not::not")]
|
|
pub growing: bool,
|
|
|
|
#[serde(skip_serializing_if = "Not::not")]
|
|
pub has_trailing_zero: bool,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub end_reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct VideoSampleEntry {
|
|
pub width: u16,
|
|
pub height: u16,
|
|
pub pasp_h_spacing: u16,
|
|
pub pasp_v_spacing: u16,
|
|
pub aspect_width: u32,
|
|
pub aspect_height: u32,
|
|
}
|
|
|
|
impl VideoSampleEntry {
|
|
fn from(e: &db::VideoSampleEntry) -> Self {
|
|
let aspect = e.aspect();
|
|
Self {
|
|
width: e.width,
|
|
height: e.height,
|
|
pasp_h_spacing: e.pasp_h_spacing,
|
|
pasp_v_spacing: e.pasp_v_spacing,
|
|
aspect_width: *aspect.numer(),
|
|
aspect_height: *aspect.denom(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ToplevelUser {
|
|
pub name: String,
|
|
pub id: i32,
|
|
pub preferences: db::json::UserPreferences,
|
|
pub session: Option<Session>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct PutUsers<'a> {
|
|
#[serde(borrow)]
|
|
pub csrf: Option<&'a str>,
|
|
pub user: UserSubset<'a>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct PostUser<'a> {
|
|
#[serde(borrow)]
|
|
pub csrf: Option<&'a str>,
|
|
pub update: Option<UserSubset<'a>>,
|
|
pub precondition: Option<UserSubset<'a>>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct DeleteUser<'a> {
|
|
#[serde(borrow)]
|
|
pub csrf: Option<&'a str>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct UserSubset<'a> {
|
|
#[serde(borrow)]
|
|
pub username: Option<&'a str>,
|
|
|
|
pub disabled: Option<bool>,
|
|
|
|
pub preferences: Option<db::json::UserPreferences>,
|
|
|
|
/// An optional password value.
|
|
///
|
|
/// `None` indicates the password does not wish to check/update the password.
|
|
/// `Some(None)` indicates the password should be absent.
|
|
#[serde(borrow, default, deserialize_with = "deserialize_some")]
|
|
pub password: Option<Option<&'a str>>,
|
|
|
|
pub permissions: Option<Permissions>,
|
|
}
|
|
|
|
impl<'a> From<&'a db::User> for UserSubset<'a> {
|
|
fn from(u: &'a db::User) -> Self {
|
|
Self {
|
|
username: Some(&u.username),
|
|
disabled: Some(u.config.disabled),
|
|
preferences: Some(u.config.preferences.clone()),
|
|
password: Some(u.has_password().then_some("(censored)")),
|
|
permissions: Some(u.permissions.clone().into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Any value that is present is considered Some value, including null.
|
|
// https://github.com/serde-rs/serde/issues/984#issuecomment-314143738
|
|
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
|
where
|
|
T: Deserialize<'de>,
|
|
D: Deserializer<'de>,
|
|
{
|
|
Deserialize::deserialize(deserializer).map(Some)
|
|
}
|
|
|
|
/// API/config analog of `Permissions` defined in `db/proto/schema.proto`.
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
#[serde(deny_unknown_fields)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Permissions {
|
|
#[serde(default)]
|
|
pub view_video: bool,
|
|
|
|
#[serde(default)]
|
|
pub read_camera_configs: bool,
|
|
|
|
#[serde(default)]
|
|
pub update_signals: bool,
|
|
|
|
#[serde(default)]
|
|
pub admin_users: bool,
|
|
}
|
|
|
|
impl From<Permissions> for db::schema::Permissions {
|
|
fn from(p: Permissions) -> Self {
|
|
Self {
|
|
view_video: p.view_video,
|
|
read_camera_configs: p.read_camera_configs,
|
|
update_signals: p.update_signals,
|
|
admin_users: p.admin_users,
|
|
special_fields: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<db::schema::Permissions> for Permissions {
|
|
fn from(p: db::schema::Permissions) -> Self {
|
|
Self {
|
|
view_video: p.view_video,
|
|
read_camera_configs: p.read_camera_configs,
|
|
update_signals: p.update_signals,
|
|
admin_users: p.admin_users,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Response to `GET /api/users/`.
|
|
#[derive(Serialize)]
|
|
pub struct GetUsersResponse<'a> {
|
|
pub users: Vec<UserWithId<'a>>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct UserWithId<'a> {
|
|
pub id: i32,
|
|
pub user: UserSubset<'a>,
|
|
}
|
|
|
|
/// Response to `PUT /api/users/`.
|
|
#[derive(Serialize)]
|
|
pub struct PutUsersResponse {
|
|
pub id: i32,
|
|
}
|