iobroker.time-tracker 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,7 @@ var import_absences = require("../db/repositories/absences");
38
38
  var import_audit = require("../db/repositories/audit");
39
39
  var import_rfid = require("../db/repositories/rfid");
40
40
  var import_auth = require("../services/auth");
41
+ var import_calendar = require("../services/calendar");
41
42
  var import_errors = require("../errors");
42
43
  var import_seed = require("../db/seed");
43
44
  var import_punch = require("../domain/punch");
@@ -377,20 +378,16 @@ function createApi(deps) {
377
378
  const emit = (event) => {
378
379
  events.publish({ ...event, atUtc: now() });
379
380
  };
380
- function requireInsideEditWindow(context, ownerId, tsUtc) {
381
- const actor = context.auth;
382
- if (!actor || actor.user.id !== ownerId || actor.permissions.includes("time.edit_other")) {
381
+ function requireActiveAdministrator(userId, next) {
382
+ if (next.isActive && next.roleKeys.includes("admin")) {
383
383
  return;
384
384
  }
385
- const days = settings.getNumber("edit_window_days", 0);
386
- if (days <= 0) {
387
- return;
388
- }
389
- if (tsUtc < now() - days * 86400) {
385
+ const remaining = users.list().some((user) => user.id !== userId && users.roles(user.id).includes("admin"));
386
+ if (!remaining) {
390
387
  throw (0, import_problem.problem)(
391
- 403,
392
- "edit_window_closed",
393
- `own punches may only be changed within ${days} day(s) (edit_window_days)`
388
+ 409,
389
+ "last_administrator",
390
+ "the last active administrator cannot be deactivated or lose the admin role"
394
391
  );
395
392
  }
396
393
  }
@@ -771,7 +768,6 @@ function createApi(deps) {
771
768
  if (tsUtc === null) {
772
769
  throw new import_errors.ValidationError("tsUtc is required");
773
770
  }
774
- requireInsideEditWindow(context, target, tsUtc);
775
771
  const timestamp = now();
776
772
  const stored = entries.insert({
777
773
  userId: target,
@@ -805,44 +801,11 @@ function createApi(deps) {
805
801
  { location: `/entries/${stored.entry.id}` }
806
802
  );
807
803
  });
808
- const ICAL_CONTENT_TYPE = "text/calendar; charset=utf-8";
809
- const CALENDAR_TTL = "PT2H";
810
- const icalText = (value) => value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
811
- const icalStamp = (seconds) => new Date(seconds * 1e3).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
812
- const dayAfter = (date) => {
813
- const [year, month, day] = date.split("-").map(Number);
814
- return new Date(Date.UTC(year, month - 1, day + 1)).toISOString().slice(0, 10);
815
- };
816
- const calendarDocument = (user, list, stamp) => {
817
- const lines = [
818
- "BEGIN:VCALENDAR",
819
- "VERSION:2.0",
820
- "PRODID:-//ioBroker//time-tracker//EN",
821
- "CALSCALE:GREGORIAN",
822
- "METHOD:PUBLISH",
823
- `X-WR-CALNAME:${icalText(`Abwesenheiten ${user.displayName}`)}`,
824
- `REFRESH-INTERVAL;VALUE=DURATION:${CALENDAR_TTL}`,
825
- `X-PUBLISHED-TTL:${CALENDAR_TTL}`
826
- ];
827
- for (const absence of list) {
828
- const portion = absence.dayPortion < 1 ? ` (${absence.dayPortion})` : "";
829
- lines.push(
830
- "BEGIN:VEVENT",
831
- `UID:absence-${absence.id}@time-tracker`,
832
- `DTSTAMP:${icalStamp(stamp)}`,
833
- `DTSTART;VALUE=DATE:${absence.dateFrom.replace(/-/g, "")}`,
834
- `DTEND;VALUE=DATE:${dayAfter(absence.dateTo).replace(/-/g, "")}`,
835
- `SUMMARY:${icalText(`${absence.typeName} (${absence.typeCode})${portion}`)}`,
836
- absence.approval === "approved" ? "STATUS:CONFIRMED" : "STATUS:TENTATIVE",
837
- absence.note ? `DESCRIPTION:${icalText(absence.note)}` : null,
838
- "TRANSP:TRANSPARENT",
839
- "END:VEVENT"
840
- );
841
- }
842
- lines.push("END:VCALENDAR");
843
- return `${lines.filter((line) => line !== null).join("\r\n")}\r
844
- `;
845
- };
804
+ const calendarEmployee = (user) => ({
805
+ id: user.id,
806
+ displayName: user.displayName,
807
+ login: user.login
808
+ });
846
809
  const publicAbsence = (absence) => {
847
810
  var _a2, _b2;
848
811
  const type = absences.findType(absence.typeId);
@@ -1052,18 +1015,29 @@ function createApi(deps) {
1052
1015
  (context) => {
1053
1016
  var _a2;
1054
1017
  const token = ((_a2 = context.query("token")) != null ? _a2 : "").trim();
1055
- const user = token === "" ? null : users.findByCalendarToken(token);
1056
- if (!user || !user.isActive) {
1018
+ const instanceToken = settings.get("calendar_token");
1019
+ const company = instanceToken !== null && instanceToken !== "" && token === instanceToken;
1020
+ const user = company || token === "" ? null : users.findByCalendarToken(token);
1021
+ if (!company && (!user || !user.isActive)) {
1057
1022
  throw (0, import_problem.problem)(401, "invalid_credentials", "the calendar token is not valid");
1058
1023
  }
1059
1024
  const stamp = now();
1060
- const today = new Date(stamp * 1e3);
1061
- const year = today.getUTCFullYear();
1062
- const list = absences.withTypesInRange(user.id, `${year - 1}-01-01`, `${year + 1}-12-31`);
1063
- return (0, import_router.binary)(200, Buffer.from(calendarDocument(user, list, stamp), "utf8"), ICAL_CONTENT_TYPE, {
1064
- "content-disposition": 'inline; filename="time-tracker.ics"',
1065
- "cache-control": "no-store"
1066
- });
1025
+ const year = new Date(stamp * 1e3).getUTCFullYear();
1026
+ const from = `${year - 1}-01-01`;
1027
+ const to = `${year + 1}-12-31`;
1028
+ const owners = user ? [user] : users.list();
1029
+ const employees = new Map(owners.map((employee) => [employee.id, calendarEmployee(employee)]));
1030
+ const list = user ? absences.withTypesInRange(user.id, from, to) : absences.allInRange(from, to);
1031
+ const name = user ? `Abwesenheiten ${user.displayName}` : import_calendar.COMPANY_CALENDAR_NAME;
1032
+ return (0, import_router.binary)(
1033
+ 200,
1034
+ Buffer.from((0, import_calendar.calendarDocument)(name, (0, import_calendar.absenceEvents)(list, employees, !user), stamp), "utf8"),
1035
+ import_calendar.ICAL_CONTENT_TYPE,
1036
+ {
1037
+ "content-disposition": 'inline; filename="time-tracker.ics"',
1038
+ "cache-control": "no-store"
1039
+ }
1040
+ );
1067
1041
  }
1068
1042
  );
1069
1043
  route("POST", "/calendar/token", { permission: "report.view_own", csrf: true }, (context) => {
@@ -1497,10 +1471,6 @@ function createApi(deps) {
1497
1471
  if (wantedTs !== null && !context.auth.permissions.includes("time.edit_other")) {
1498
1472
  throw (0, import_problem.problem)(403, "permission_denied", "request rejected (permission_denied: time.edit_other)");
1499
1473
  }
1500
- if (wantedTs !== null) {
1501
- requireInsideEditWindow(context, existing.userId, existing.tsUtc);
1502
- requireInsideEditWindow(context, existing.userId, wantedTs);
1503
- }
1504
1474
  const target = users.findById(existing.userId);
1505
1475
  if (!target) {
1506
1476
  throw new import_errors.NotFoundError(`user ${existing.userId} not found`);
@@ -1702,12 +1672,13 @@ function createApi(deps) {
1702
1672
  });
1703
1673
  });
1704
1674
  route("PATCH", "/users/:id", { permission: "user.edit", csrf: true }, (context) => {
1705
- var _a2, _b2, _c2;
1675
+ var _a2, _b2, _c2, _d2;
1706
1676
  if (!context.auth) {
1707
1677
  throw (0, import_problem.problem)(401, "no_session", "request rejected (no_session)");
1708
1678
  }
1709
1679
  const id = numberParam(context, "id");
1710
- if (!users.findById(id)) {
1680
+ const target = users.findById(id);
1681
+ if (!target) {
1711
1682
  throw new import_errors.NotFoundError(`user ${id} not found`);
1712
1683
  }
1713
1684
  const body = context.jsonBody();
@@ -1770,19 +1741,25 @@ function createApi(deps) {
1770
1741
  now: now()
1771
1742
  });
1772
1743
  }
1744
+ if (patch.isActive === false || roleKeys !== null) {
1745
+ requireActiveAdministrator(id, {
1746
+ isActive: (_b2 = patch.isActive) != null ? _b2 : target.isActive,
1747
+ roleKeys: roleKeys != null ? roleKeys : users.roles(id)
1748
+ });
1749
+ }
1773
1750
  const updated = users.update({
1774
1751
  id,
1775
1752
  patch,
1776
1753
  reason: optionalString(body, "reason"),
1777
1754
  actorId: context.auth.user.id,
1778
- actorIp: (_b2 = context.request.remoteAddress) != null ? _b2 : null,
1755
+ actorIp: (_c2 = context.request.remoteAddress) != null ? _c2 : null,
1779
1756
  now: now()
1780
1757
  });
1781
1758
  const roles = roleKeys ? users.setRoles({
1782
1759
  userId: id,
1783
1760
  roleKeys,
1784
1761
  actorId: context.auth.user.id,
1785
- actorIp: (_c2 = context.request.remoteAddress) != null ? _c2 : null,
1762
+ actorIp: (_d2 = context.request.remoteAddress) != null ? _d2 : null,
1786
1763
  now: now()
1787
1764
  }) : users.roles(id);
1788
1765
  return (0, import_router.json)(200, { user: { ...publicUser(updated), roles } });