mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
event info
This commit is contained in:
@@ -21,8 +21,11 @@
|
|||||||
<table class="table table-hover" *ngIf="events && events.length">
|
<table class="table table-hover" *ngIf="events && events.length">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="border-top-0">{{'timestamp' | i18n}}</th>
|
<th class="border-top-0" width="210">{{'timestamp' | i18n}}</th>
|
||||||
<th class="border-top-0">{{'user' | i18n}}</th>
|
<th class="border-top-0" width="40">
|
||||||
|
<span class="sr-only">{{'device' | i18n}}</span>
|
||||||
|
</th>
|
||||||
|
<th class="border-top-0" width="150">{{'user' | i18n}}</th>
|
||||||
<th class="border-top-0">{{'event' | i18n}}</th>
|
<th class="border-top-0">{{'event' | i18n}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -30,11 +33,12 @@
|
|||||||
<tr *ngFor="let e of events">
|
<tr *ngFor="let e of events">
|
||||||
<td>{{e.date | date:'medium'}}</td>
|
<td>{{e.date | date:'medium'}}</td>
|
||||||
<td>
|
<td>
|
||||||
{{e.userId}}
|
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}"></i>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{e.type}}
|
{{e.userName}}
|
||||||
</td>
|
</td>
|
||||||
|
<td [innerHTML]="e.message"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
|||||||
|
|
||||||
import { EventService } from '../../services/event.service';
|
import { EventService } from '../../services/event.service';
|
||||||
|
|
||||||
|
import { EventResponse } from 'jslib/models/response/eventResponse';
|
||||||
|
import { ListResponse } from 'jslib/models/response/listResponse';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-org-events',
|
selector: 'app-org-events',
|
||||||
templateUrl: 'events.component.html',
|
templateUrl: 'events.component.html',
|
||||||
@@ -56,6 +59,7 @@ export class EventsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
let response: ListResponse<EventResponse>;
|
||||||
try {
|
try {
|
||||||
const promise = this.apiService.getEventsOrganization(this.organizationId, dates[0], dates[1],
|
const promise = this.apiService.getEventsOrganization(this.organizationId, dates[0], dates[1],
|
||||||
clearExisting ? null : this.continuationToken);
|
clearExisting ? null : this.continuationToken);
|
||||||
@@ -64,14 +68,15 @@ export class EventsComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.morePromise = promise;
|
this.morePromise = promise;
|
||||||
}
|
}
|
||||||
const response = await promise;
|
response = await promise;
|
||||||
|
} catch { }
|
||||||
|
|
||||||
this.continuationToken = response.continuationToken;
|
this.continuationToken = response.continuationToken;
|
||||||
const events = response.data.map((r) => {
|
const events = response.data.map((r) => {
|
||||||
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
||||||
const eventInfo: any = {};
|
const eventInfo = this.eventService.getEventInfo(r);
|
||||||
const htmlMessage = '';
|
|
||||||
return {
|
return {
|
||||||
message: htmlMessage,
|
message: eventInfo.message,
|
||||||
appIcon: eventInfo.appIcon,
|
appIcon: eventInfo.appIcon,
|
||||||
appName: eventInfo.appName,
|
appName: eventInfo.appName,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
@@ -80,12 +85,13 @@ export class EventsComponent implements OnInit {
|
|||||||
ip: r.ipAddress,
|
ip: r.ipAddress,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!clearExisting && this.events != null && this.events.length > 0) {
|
if (!clearExisting && this.events != null && this.events.length > 0) {
|
||||||
this.events = this.events.concat(events);
|
this.events = this.events.concat(events);
|
||||||
} else {
|
} else {
|
||||||
this.events = events;
|
this.events = events;
|
||||||
}
|
}
|
||||||
} catch { }
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.morePromise = null;
|
this.morePromise = null;
|
||||||
this.refreshPromise = null;
|
this.refreshPromise = null;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
|
||||||
|
import { DeviceType } from 'jslib/enums/deviceType';
|
||||||
|
import { EventType } from 'jslib/enums/eventType';
|
||||||
|
|
||||||
|
import { EventResponse } from 'jslib/models/response/eventResponse';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventService {
|
export class EventService {
|
||||||
constructor(private i18nService: I18nService) { }
|
constructor(private i18nService: I18nService) { }
|
||||||
@@ -23,6 +28,210 @@ export class EventService {
|
|||||||
return [start.toISOString(), end.toISOString()];
|
return [start.toISOString(), end.toISOString()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEventInfo(ev: EventResponse, options = new EventOptions()): EventInfo {
|
||||||
|
const appInfo = this.getAppInfo(ev.deviceType);
|
||||||
|
return {
|
||||||
|
message: this.getEventMessage(ev, options),
|
||||||
|
appIcon: appInfo[0],
|
||||||
|
appName: appInfo[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEventMessage(ev: EventResponse, options: EventOptions) {
|
||||||
|
let msg = '';
|
||||||
|
switch (ev.type) {
|
||||||
|
// User
|
||||||
|
case EventType.User_LoggedIn:
|
||||||
|
msg = this.i18nService.t('loggedIn');
|
||||||
|
break;
|
||||||
|
case EventType.User_ChangedPassword:
|
||||||
|
msg = this.i18nService.t('changedPassword');
|
||||||
|
break;
|
||||||
|
case EventType.User_Enabled2fa:
|
||||||
|
msg = this.i18nService.t('enabled2fa');
|
||||||
|
break;
|
||||||
|
case EventType.User_Disabled2fa:
|
||||||
|
msg = this.i18nService.t('disabled2fa');
|
||||||
|
break;
|
||||||
|
case EventType.User_Recovered2fa:
|
||||||
|
msg = this.i18nService.t('recovered2fa');
|
||||||
|
break;
|
||||||
|
case EventType.User_FailedLogIn:
|
||||||
|
msg = this.i18nService.t('failedLogin');
|
||||||
|
break;
|
||||||
|
case EventType.User_FailedLogIn2fa:
|
||||||
|
msg = this.i18nService.t('failedLogin2fa');
|
||||||
|
break;
|
||||||
|
// Cipher
|
||||||
|
case EventType.Cipher_Created:
|
||||||
|
msg = this.i18nService.t('createdThing', this.i18nService.t('item').toLocaleLowerCase(),
|
||||||
|
this.formatCipherId(ev, options));
|
||||||
|
break;
|
||||||
|
case EventType.Cipher_Updated:
|
||||||
|
msg = this.i18nService.t('editedThing', this.i18nService.t('item').toLocaleLowerCase(),
|
||||||
|
this.formatCipherId(ev, options));
|
||||||
|
break;
|
||||||
|
case EventType.Cipher_Deleted:
|
||||||
|
msg = this.i18nService.t('deletedThing', this.i18nService.t('item').toLocaleLowerCase(),
|
||||||
|
this.formatCipherId(ev, options));
|
||||||
|
break;
|
||||||
|
case EventType.Cipher_AttachmentCreated:
|
||||||
|
msg = this.i18nService.t('createdAttachmentForItem', this.formatCipherId(ev, options));
|
||||||
|
break;
|
||||||
|
case EventType.Cipher_AttachmentDeleted:
|
||||||
|
msg = this.i18nService.t('deletedAttachmentForItem', this.formatCipherId(ev, options));
|
||||||
|
break;
|
||||||
|
case EventType.Cipher_Shared:
|
||||||
|
msg = this.i18nService.t('sharedThing', this.i18nService.t('item').toLocaleLowerCase(),
|
||||||
|
this.formatCipherId(ev, options));
|
||||||
|
break;
|
||||||
|
case EventType.Cipher_UpdatedCollections:
|
||||||
|
msg = this.i18nService.t('editedCollectionsForItem', this.formatCipherId(ev, options));
|
||||||
|
break;
|
||||||
|
// Collection
|
||||||
|
case EventType.Collection_Created:
|
||||||
|
msg = this.i18nService.t('createdThing', this.i18nService.t('collection').toLocaleLowerCase(),
|
||||||
|
this.formatCollectionId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.Collection_Updated:
|
||||||
|
msg = this.i18nService.t('editedThing', this.i18nService.t('collection').toLocaleLowerCase(),
|
||||||
|
this.formatCollectionId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.Collection_Deleted:
|
||||||
|
msg = this.i18nService.t('deletedThing', this.i18nService.t('collection').toLocaleLowerCase(),
|
||||||
|
this.formatCollectionId(ev));
|
||||||
|
break;
|
||||||
|
// Group
|
||||||
|
case EventType.Group_Created:
|
||||||
|
msg = this.i18nService.t('createdThing', this.i18nService.t('group').toLocaleLowerCase(),
|
||||||
|
this.formatGroupId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.Group_Updated:
|
||||||
|
msg = this.i18nService.t('editedThing', this.i18nService.t('group').toLocaleLowerCase(),
|
||||||
|
this.formatGroupId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.Group_Deleted:
|
||||||
|
msg = this.i18nService.t('deletedThing', this.i18nService.t('group').toLocaleLowerCase(),
|
||||||
|
this.formatGroupId(ev));
|
||||||
|
break;
|
||||||
|
// Org user
|
||||||
|
case EventType.OrganizationUser_Invited:
|
||||||
|
msg = this.i18nService.t('invitedUser', this.formatOrgUserId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.OrganizationUser_Confirmed:
|
||||||
|
msg = this.i18nService.t('confirmedUser', this.formatOrgUserId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.OrganizationUser_Updated:
|
||||||
|
msg = this.i18nService.t('editedThing', this.i18nService.t('user').toLocaleLowerCase(),
|
||||||
|
this.formatOrgUserId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.OrganizationUser_Removed:
|
||||||
|
msg = this.i18nService.t('removedThing', this.i18nService.t('user').toLocaleLowerCase(),
|
||||||
|
this.formatOrgUserId(ev));
|
||||||
|
break;
|
||||||
|
case EventType.OrganizationUser_UpdatedGroups:
|
||||||
|
msg = this.i18nService.t('editedGroupsForUser', this.formatOrgUserId(ev));
|
||||||
|
break;
|
||||||
|
// Org
|
||||||
|
case EventType.Organization_Updated:
|
||||||
|
msg = this.i18nService.t('editedOrgSettings');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return msg === '' ? null : msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAppInfo(deviceType: DeviceType): [string, string] {
|
||||||
|
switch (deviceType) {
|
||||||
|
case DeviceType.Android:
|
||||||
|
return ['fa-android', this.i18nService.t('mobile') + ' - Android'];
|
||||||
|
case DeviceType.iOS:
|
||||||
|
return ['fa-apple', this.i18nService.t('mobile') + ' - iOS'];
|
||||||
|
case DeviceType.UWP:
|
||||||
|
return ['fa-windows', this.i18nService.t('mobile') + ' - Windows'];
|
||||||
|
case DeviceType.ChromeExtension:
|
||||||
|
return ['fa-chrome', this.i18nService.t('extension') + ' - Chrome'];
|
||||||
|
case DeviceType.FirefoxExtension:
|
||||||
|
return ['fa-firefox', this.i18nService.t('extension') + ' - Firefox'];
|
||||||
|
case DeviceType.OperaExtension:
|
||||||
|
return ['fa-opera', this.i18nService.t('extension') + ' - Opera'];
|
||||||
|
case DeviceType.EdgeExtension:
|
||||||
|
return ['fa-edge', this.i18nService.t('extension') + ' - Edge'];
|
||||||
|
case DeviceType.VivaldiExtension:
|
||||||
|
return ['fa-puzzle-piece', this.i18nService.t('extension') + ' - Vivaldi'];
|
||||||
|
case DeviceType.SafariExtension:
|
||||||
|
return ['fa-safari', this.i18nService.t('extension') + ' - Safari'];
|
||||||
|
case DeviceType.WindowsDesktop:
|
||||||
|
return ['fa-windows', this.i18nService.t('desktop') + ' - Windows'];
|
||||||
|
case DeviceType.MacOsDesktop:
|
||||||
|
return ['fa-apple', this.i18nService.t('desktop') + ' - macOS'];
|
||||||
|
case DeviceType.LinuxDesktop:
|
||||||
|
return ['fa-linux', this.i18nService.t('desktop') + ' - Linux'];
|
||||||
|
case DeviceType.ChromeBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - Chrome'];
|
||||||
|
case DeviceType.FirefoxBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - Firefox'];
|
||||||
|
case DeviceType.OperaBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - Opera'];
|
||||||
|
case DeviceType.SafariBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - Safari'];
|
||||||
|
case DeviceType.VivaldiBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - Vivaldi'];
|
||||||
|
case DeviceType.EdgeBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - Edge'];
|
||||||
|
case DeviceType.IEBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - IE'];
|
||||||
|
case DeviceType.UnknownBrowser:
|
||||||
|
return ['fa-globe', this.i18nService.t('webVault') + ' - ' + this.i18nService.t('unknown')];
|
||||||
|
default:
|
||||||
|
return ['fa-globe', this.i18nService.t('unknown')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatCipherId(ev: EventResponse, options: EventOptions) {
|
||||||
|
const shortId = this.getShortId(ev.cipherId);
|
||||||
|
if (ev.organizationId == null || !options.cipherInfo) {
|
||||||
|
return '<code>' + shortId + '</code>';
|
||||||
|
}
|
||||||
|
const a = this.makeAnchor(shortId);
|
||||||
|
a.setAttribute('href', '#/organizations/' + ev.organizationId + '/vault?search=' + shortId +
|
||||||
|
'&viewEvents=' + ev.cipherId);
|
||||||
|
return a.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatGroupId(ev: EventResponse) {
|
||||||
|
const shortId = this.getShortId(ev.groupId);
|
||||||
|
const a = this.makeAnchor(shortId);
|
||||||
|
a.setAttribute('href', '#/organizations/' + ev.organizationId + '/manage/groups?search=' + shortId);
|
||||||
|
return a.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatCollectionId(ev: EventResponse) {
|
||||||
|
const shortId = this.getShortId(ev.collectionId);
|
||||||
|
const a = this.makeAnchor(shortId);
|
||||||
|
a.setAttribute('href', '#/organizations/' + ev.organizationId + '/manage/collections?search=' + shortId);
|
||||||
|
return a.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatOrgUserId(ev: EventResponse) {
|
||||||
|
const shortId = this.getShortId(ev.organizationUserId);
|
||||||
|
const a = this.makeAnchor(shortId);
|
||||||
|
a.setAttribute('href', '#/organizations/' + ev.organizationId + '/manage/people?search=' + shortId);
|
||||||
|
return a.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeAnchor(shortId: string) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.title = this.i18nService.t('view');
|
||||||
|
a.innerHTML = '<code>' + shortId + '</code>';
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getShortId(id: string) {
|
||||||
|
return id.substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
private toDateTimeLocalString(date: Date) {
|
private toDateTimeLocalString(date: Date) {
|
||||||
return date.getFullYear() +
|
return date.getFullYear() +
|
||||||
'-' + this.pad(date.getMonth() + 1) +
|
'-' + this.pad(date.getMonth() + 1) +
|
||||||
@@ -36,3 +245,13 @@ export class EventService {
|
|||||||
return (norm < 10 ? '0' : '') + norm;
|
return (norm < 10 ? '0' : '') + norm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class EventInfo {
|
||||||
|
message: string;
|
||||||
|
appIcon: string;
|
||||||
|
appName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventOptions {
|
||||||
|
cipherInfo = true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1747,5 +1747,178 @@
|
|||||||
},
|
},
|
||||||
"loadMore": {
|
"loadMore": {
|
||||||
"message": "Load More"
|
"message": "Load More"
|
||||||
|
},
|
||||||
|
"mobile": {
|
||||||
|
"message": "Mobile",
|
||||||
|
"description": "Mobile app"
|
||||||
|
},
|
||||||
|
"extension": {
|
||||||
|
"message": "Extension",
|
||||||
|
"description": "Browser extension/addon"
|
||||||
|
},
|
||||||
|
"desktop": {
|
||||||
|
"message": "Desktop",
|
||||||
|
"description": "Desktop app"
|
||||||
|
},
|
||||||
|
"webVault": {
|
||||||
|
"message": "Web Vault"
|
||||||
|
},
|
||||||
|
"loggedIn": {
|
||||||
|
"message": "Logged in."
|
||||||
|
},
|
||||||
|
"changedPassword": {
|
||||||
|
"message": "Changed account password."
|
||||||
|
},
|
||||||
|
"enabled2fa": {
|
||||||
|
"message": "Enabled two-step login."
|
||||||
|
},
|
||||||
|
"disabled2fa": {
|
||||||
|
"message": "Disabled two-step login."
|
||||||
|
},
|
||||||
|
"recovered2fa": {
|
||||||
|
"message": "Recovered account from two-step login."
|
||||||
|
},
|
||||||
|
"failedLogin": {
|
||||||
|
"message": "Login attempt failed with incorrect password."
|
||||||
|
},
|
||||||
|
"failedLogin2fa": {
|
||||||
|
"message": "Login attempt failed with incorrect two-step login."
|
||||||
|
},
|
||||||
|
"editedOrgSettings": {
|
||||||
|
"message": "Edited organization settings."
|
||||||
|
},
|
||||||
|
"createdThing": {
|
||||||
|
"message": "Created $THING$ $ID$.",
|
||||||
|
"description": "Created item abe89f32.",
|
||||||
|
"placeholders": {
|
||||||
|
"thing": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "item"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editedThing": {
|
||||||
|
"message": "Edited $THING$ $ID$.",
|
||||||
|
"description": "Edited item abe89f32.",
|
||||||
|
"placeholders": {
|
||||||
|
"thing": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "item"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deletedThing": {
|
||||||
|
"message": "Deleted $THING$ $ID$.",
|
||||||
|
"description": "Deleted item abe89f32.",
|
||||||
|
"placeholders": {
|
||||||
|
"thing": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "item"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharedThing": {
|
||||||
|
"message": "Shared $THING$ $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"thing": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "item"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"removedThing": {
|
||||||
|
"message": "Removed $THING$ $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"thing": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "item"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdAttachmentForItem": {
|
||||||
|
"message": "Created attachment for item $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deletedAttachmentForItem": {
|
||||||
|
"message": "Deleted attachment for item $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editedCollectionsForItem": {
|
||||||
|
"message": "Edited collections for item $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"invitedUser": {
|
||||||
|
"message": "Invited user $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmedUser": {
|
||||||
|
"message": "Confirmed user $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editedGroupsForUser": {
|
||||||
|
"message": "Edited groups for user $ID$.",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "abe89f32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"item": {
|
||||||
|
"message": "Item"
|
||||||
|
},
|
||||||
|
"collection": {
|
||||||
|
"message": "Collection"
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"message": "Group"
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"message": "Device"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user