feat(keycloak) Optional Keycloak integration via OpenId Connect (OIDC) for Identity and Access Management (#227)
This commit is contained in:
parent
03d0798459
commit
33755f7b8b
@ -25,7 +25,9 @@ reactive-var@1.0.11
|
||||
reactive-dict@1.2.0
|
||||
standard-minifier-css@1.4.1
|
||||
standard-minifier-js@2.3.4
|
||||
|
||||
accounts-base
|
||||
accounts-oauth
|
||||
oauth-encryption
|
||||
|
||||
# OHIF Packages
|
||||
ohif:polyfill
|
||||
@ -42,6 +44,8 @@ ohif:dicom-services
|
||||
ohif:dicomweb-client
|
||||
ohif:hanging-protocols
|
||||
ohif:metadata
|
||||
ohif:user
|
||||
ohif:user-keycloak
|
||||
|
||||
fortawesome:fontawesome
|
||||
momentjs:moment@2.15.1
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
accounts-base@1.4.2
|
||||
accounts-oauth@1.1.15
|
||||
accounts-password@1.5.1
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
@ -38,6 +40,7 @@ ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.7.1
|
||||
ecmascript-runtime-server@0.7.0
|
||||
ejson@1.1.0
|
||||
email@1.2.3
|
||||
es5-shim@4.8.0
|
||||
fastclick@1.0.13
|
||||
fortawesome:fontawesome@4.7.0
|
||||
@ -77,8 +80,15 @@ momentjs:moment@2.22.2
|
||||
mongo@1.5.1
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
mxab:accounts-keycloak@0.0.2
|
||||
mxab:keycloak-loader@0.0.2
|
||||
mxab:keycloak-oauth@0.0.2
|
||||
natestrauser:select2@4.0.3
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@3.0.7
|
||||
oauth@1.2.3
|
||||
oauth-encryption@1.3.0
|
||||
oauth2@1.2.0
|
||||
observe-sequence@1.0.16
|
||||
ohif:commands@0.0.1
|
||||
ohif:core@0.0.1
|
||||
@ -98,6 +108,8 @@ ohif:studies@0.0.1
|
||||
ohif:study-list@0.0.1
|
||||
ohif:themes@0.0.1
|
||||
ohif:themes-common@0.0.1
|
||||
ohif:user@0.0.1
|
||||
ohif:user-keycloak@0.0.1
|
||||
ohif:viewerbase@0.0.1
|
||||
ohif:wadoproxy@0.0.1
|
||||
ordered-dict@1.1.0
|
||||
@ -113,11 +125,13 @@ retry@1.1.0
|
||||
routepolicy@1.0.13
|
||||
service-configuration@1.0.11
|
||||
session@1.1.7
|
||||
sha@1.0.9
|
||||
shell-server@0.3.1
|
||||
silentcicero:jszip@0.0.4
|
||||
socket-stream-client@0.2.2
|
||||
spacebars@1.0.15
|
||||
spacebars-compiler@1.1.3
|
||||
srp@1.0.10
|
||||
standard-app-packages@1.0.9
|
||||
standard-minifier-css@1.4.1
|
||||
standard-minifier-js@2.3.4
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
@ -8,7 +9,7 @@ Template.ohifViewer.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.headerClasses = new ReactiveVar('');
|
||||
|
||||
OHIF.header.dropdown.setItems([{
|
||||
const headerItems = [{
|
||||
action: () => OHIF.ui.showDialog('serverInformationModal'),
|
||||
text: 'Server Information',
|
||||
icon: 'fa fa-server fa-lg',
|
||||
@ -28,7 +29,18 @@ Template.ohifViewer.onCreated(() => {
|
||||
action: () => OHIF.ui.showDialog('aboutModal'),
|
||||
text: 'About',
|
||||
icon: 'fa fa-info'
|
||||
}]);
|
||||
}];
|
||||
|
||||
if (Meteor.user()) {
|
||||
items.push({
|
||||
action: OHIF.user.logout,
|
||||
text: 'Logout',
|
||||
iconClasses: 'logout',
|
||||
iconSvgUse: 'packages/ohif_viewerbase/assets/user-menu-icons.svg#logout'
|
||||
});
|
||||
}
|
||||
|
||||
OHIF.header.dropdown.setItems(headerItems);
|
||||
|
||||
instance.autorun(() => {
|
||||
const currentRoute = Router.current();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstoneWADOImageLoader } from 'meteor/ohif:cornerstone';
|
||||
import { Accounts } from "meteor/accounts-base";
|
||||
|
||||
Meteor.startup(function() {
|
||||
const maxWebWorkers = Math.max(navigator.hardwareConcurrency - 1, 1);
|
||||
@ -19,4 +20,21 @@ Meteor.startup(function() {
|
||||
};
|
||||
|
||||
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
||||
|
||||
cornerstoneWADOImageLoader.configure({
|
||||
beforeSend: function(xhr) {
|
||||
const userId = Meteor.userId();
|
||||
const accessToken = OHIF.user.getAccessToken();
|
||||
|
||||
if (accessToken) {
|
||||
xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`);
|
||||
} else {
|
||||
const loginToken = Accounts._storedLoginToken();
|
||||
if (userId && loginToken) {
|
||||
xhr.setRequestHeader("x-user-id", userId);
|
||||
xhr.setRequestHeader("x-auth-token", loginToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
7
Packages/ohif-dicomweb-client/src/getAccessToken.js
Normal file
7
Packages/ohif-dicomweb-client/src/getAccessToken.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default function getAccessToken() {
|
||||
if (!global.window || !window.sessionStorage || !sessionStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
return sessionStorage.token;
|
||||
}
|
||||
@ -6,6 +6,7 @@ import getModalities from './getModalities.js';
|
||||
import getName from './getName.js';
|
||||
import getNumber from './getNumber.js';
|
||||
import getString from './getString.js';
|
||||
import getAccessToken from './getAccessToken.js';
|
||||
import makeRequest from './makeRequest.js';
|
||||
|
||||
const DICOMWeb = {
|
||||
@ -17,6 +18,7 @@ const DICOMWeb = {
|
||||
getName,
|
||||
getNumber,
|
||||
getString,
|
||||
getAccessToken,
|
||||
makeRequest
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Meteor } from "meteor/meteor";
|
||||
import URL from 'url-parse';
|
||||
import 'isomorphic-fetch';
|
||||
import getAccessToken from './getAccessToken.js';
|
||||
|
||||
async function makeRequest(url, options) {
|
||||
const parsed = new URL(url);
|
||||
@ -10,8 +11,7 @@ async function makeRequest(url, options) {
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// TODO: Clean this up
|
||||
const accessToken = false //Meteor.user().services.keycloak.accessToken;
|
||||
const accessToken = getAccessToken();
|
||||
if (accessToken) {
|
||||
requestOpt.headers = {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
|
||||
@ -255,6 +255,11 @@ export const PublicServerConfig = new SimpleSchema({
|
||||
label: 'Creates demo user on startup and show TestDrive button',
|
||||
defaultValue: true
|
||||
},
|
||||
userAuthenticationRoutesEnabled: {
|
||||
type: Boolean,
|
||||
label: 'Enables routing to /login page.',
|
||||
defaultValue: false,
|
||||
},
|
||||
ui: {
|
||||
type: UISettings,
|
||||
label: 'UI Settings'
|
||||
|
||||
@ -101,8 +101,6 @@ OHIF.studies.services.QIDO.Studies = (server, filter) => {
|
||||
const url = filterToQIDOURL(server, filter);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.warn(DICOMWeb);
|
||||
|
||||
DICOMWeb.getJSON(url, server.requestOptions).then(result => {
|
||||
const studies = resultDataToStudies(result);
|
||||
|
||||
|
||||
10
Packages/ohif-user-keycloak/client/main.js
Normal file
10
Packages/ohif-user-keycloak/client/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
if (!Meteor.settings.public ||
|
||||
!Meteor.settings.public.custom ||
|
||||
!Meteor.settings.public.custom.keycloak) {
|
||||
|
||||
console.log('To use the ohif-user-keycloak package, you must add relevant Keycloak settings to Meteor.settings.public.custom.keycloak (client-side).');
|
||||
} else {
|
||||
require('../imports/client/index.js')
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
<template name="keycloakLoginButton">
|
||||
<hr/>
|
||||
<button class="form-control btn js-login-keycloak">Login with Keycloak</button>
|
||||
</template>
|
||||
@ -0,0 +1,18 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
import './keycloakLoginButton.html';
|
||||
|
||||
Template.keycloakLoginButton.events({
|
||||
'click .js-login-keycloak'() {
|
||||
Meteor.loginWithMeteorKeycloak({}, function(error) {
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
Router.go('/studylist');
|
||||
});
|
||||
}
|
||||
});
|
||||
25
Packages/ohif-user-keycloak/imports/client/handlers.js
Normal file
25
Packages/ohif-user-keycloak/imports/client/handlers.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Meteor } from "meteor/meteor";
|
||||
|
||||
OHIF.user.getAccessToken = () => {
|
||||
const user = Meteor.user();
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
return user.services.keycloak.accessToken;
|
||||
};
|
||||
|
||||
Accounts.onLogin(() => {
|
||||
Meteor.subscribe('user.services.keycloak', () => {
|
||||
sessionStorage.token = OHIF.user.getAccessToken();
|
||||
});
|
||||
});
|
||||
|
||||
Accounts.onLogout(() => {
|
||||
const authServerUrl = Meteor.settings.public.custom.keycloak.authServerUrl;
|
||||
const realm = Meteor.settings.public.custom.keycloak.realmName;
|
||||
const redirectUri = Meteor.absoluteUrl('login');
|
||||
const logoutUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/logout?redirect_uri=${redirectUri}`;
|
||||
window.location = logoutUrl;
|
||||
});
|
||||
3
Packages/ohif-user-keycloak/imports/client/index.js
Normal file
3
Packages/ohif-user-keycloak/imports/client/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import './handlers';
|
||||
import './setup.js';
|
||||
import './components/keycloakLoginButton.js';
|
||||
30
Packages/ohif-user-keycloak/imports/client/setup.js
Normal file
30
Packages/ohif-user-keycloak/imports/client/setup.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Router.waitOn(function() {
|
||||
return [
|
||||
Meteor.subscribe('user.services.keycloak'),
|
||||
];
|
||||
}, { except: ['userLogin'] });
|
||||
|
||||
Router.onBeforeAction(function() {
|
||||
// Check if user is signed in
|
||||
if (!Meteor.userId() && !Meteor.loggingIn()) {
|
||||
this.redirect('userLogin');
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}, {
|
||||
except: ['userLogin', 'entrySignUp', 'forgotPassword', 'resetPassword']
|
||||
});
|
||||
|
||||
OHIF.user.additionalLoginButtons = OHIF.user.additionalLoginButtons || [];
|
||||
|
||||
OHIF.user.additionalLoginButtons.push({
|
||||
template: 'keycloakLoginButton'
|
||||
});
|
||||
|
||||
if (!Meteor.settings.public.userAuthenticationRoutesEnabled) {
|
||||
OHIF.log.error('Please set Meteor.settings.public.userAuthenticationRoutesEnabled=true');
|
||||
}
|
||||
51
Packages/ohif-user-keycloak/imports/server/setup.js
Normal file
51
Packages/ohif-user-keycloak/imports/server/setup.js
Normal file
@ -0,0 +1,51 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { ServiceConfiguration } from 'meteor/service-configuration';
|
||||
import { Accounts } from "meteor/accounts-base";
|
||||
|
||||
console.log('Keycloak settings were found! Enabling Keycloak integration.');
|
||||
|
||||
ServiceConfiguration.configurations.upsert(
|
||||
{ service: 'keycloak' },
|
||||
{
|
||||
$set: {
|
||||
"realm": Meteor.settings.public.custom.keycloak.realmName,
|
||||
"auth-server-url": Meteor.settings.public.custom.keycloak.authServerUrl,
|
||||
"auth_redirect_uri": Meteor.settings.keycloak.authRedirectUri,
|
||||
"ssl-required": Meteor.settings.keycloak.sslRequired,
|
||||
"resource": Meteor.settings.keycloak.clientId,
|
||||
"client_id": Meteor.settings.keycloak.clientId,
|
||||
"loginStyle": Meteor.settings.keycloak.loginStyle,
|
||||
"secret": Meteor.settings.keycloak.clientSecret,
|
||||
"realm-public-key": Meteor.settings.keycloak.realmPublicKey,
|
||||
"public-client": false,
|
||||
"use-resource-role-mappings": false,
|
||||
"bearer-only": false,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Meteor.publish('user.services.keycloak', function() {
|
||||
const userId = this.userId;
|
||||
if (!userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Meteor.users.find(userId, {
|
||||
fields: {
|
||||
'services.keycloak': 1
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Accounts.onLogout(({ user }) => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Erase any Keycloak token that exists
|
||||
Meteor.users.update(user._id, {
|
||||
$unset: {
|
||||
'services.keycloak': 1
|
||||
}
|
||||
});
|
||||
});
|
||||
26
Packages/ohif-user-keycloak/package.js
Normal file
26
Packages/ohif-user-keycloak/package.js
Normal file
@ -0,0 +1,26 @@
|
||||
Package.describe({
|
||||
name: 'ohif:user-keycloak',
|
||||
summary: 'OHIF Integration with Keycloak for Identity and Access Management',
|
||||
version: '0.0.1'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.versionsFrom('1.6');
|
||||
|
||||
api.use('templating');
|
||||
api.use('ecmascript');
|
||||
api.use('service-configuration');
|
||||
api.use('accounts-base');
|
||||
|
||||
// Our custom packages
|
||||
api.use('ohif:core');
|
||||
api.use('ohif:user');
|
||||
|
||||
api.use('mxab:keycloak-oauth@0.0.2');
|
||||
api.use('mxab:keycloak-loader@0.0.2');
|
||||
api.use('mxab:accounts-keycloak');
|
||||
|
||||
// Client imports
|
||||
api.mainModule('client/main.js', 'client');
|
||||
api.mainModule('server/main.js', 'server');
|
||||
});
|
||||
11
Packages/ohif-user-keycloak/server/main.js
Normal file
11
Packages/ohif-user-keycloak/server/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
if (!Meteor.settings.public ||
|
||||
!Meteor.settings.public.custom ||
|
||||
!Meteor.settings.public.custom.keycloak ||
|
||||
!Meteor.settings.keycloak) {
|
||||
|
||||
console.log('To use the ohif-user-keycloak package, you must add relevant Keycloak settings to Meteor.settings.keycloak (server-side).');
|
||||
} else {
|
||||
require('../imports/server/setup.js')
|
||||
}
|
||||
@ -12,6 +12,10 @@
|
||||
{{>section 'userLoginFieldsAfter'}}
|
||||
<hr class="m-t-0 m-b-2">
|
||||
{{#button action='login' class='form-control btn btn-primary'}}Enter{{/button}}
|
||||
|
||||
{{ #each additionalLoginButtons }}
|
||||
{{>Template.dynamic template=template}}
|
||||
{{ /each }}
|
||||
{{/form}}
|
||||
</div>
|
||||
{{>section 'userLoginAfter'}}
|
||||
|
||||
@ -33,3 +33,9 @@ Template.userLogin.onCreated(() => {
|
||||
|
||||
instance.schema = OHIF.user.schema;
|
||||
});
|
||||
|
||||
Template.userLogin.helpers({
|
||||
additionalLoginButtons() {
|
||||
return OHIF.user.additionalLoginButtons || [];
|
||||
}
|
||||
})
|
||||
|
||||
5
Packages/ohif-user/client/lib/getAccessToken.js
Normal file
5
Packages/ohif-user/client/lib/getAccessToken.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
OHIF.user.getAccessToken = () => {};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import './data';
|
||||
import './getName';
|
||||
import './getAccessToken';
|
||||
import './login';
|
||||
import './logout';
|
||||
import './schema';
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
|
||||
Router.route('/login', function() {
|
||||
this.layout('mainLayout', { data: {} });
|
||||
this.render('userLogin');
|
||||
}, { name: 'userLogin' });
|
||||
if (Meteor.settings.public.userAuthenticationRoutesEnabled === true) {
|
||||
Router.route('/login', function() {
|
||||
this.layout('layout', { data: {} });
|
||||
this.render('userLogin');
|
||||
}, { name: 'userLogin' });
|
||||
}
|
||||
|
||||
26
Packages/ohif-viewerbase/assets/user-menu-icons.svg
Normal file
26
Packages/ohif-viewerbase/assets/user-menu-icons.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<symbol id="theme" viewBox="0 0 24 24">
|
||||
<title>Theme</title>
|
||||
<path d="M17.484 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM14.484 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM9.516 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM6.516 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM12 3c4.969 0 9 3.609 9 8.016 0 2.766-2.25 4.969-5.016 4.969h-1.734c-0.844 0-1.5 0.656-1.5 1.5 0 0.375 0.141 0.703 0.375 0.984s0.375 0.656 0.375 1.031c0 0.844-0.656 1.5-1.5 1.5-4.969 0-9-4.031-9-9s4.031-9 9-9z"></path>
|
||||
</symbol>
|
||||
<symbol id="log" viewBox="0 0 24 24">
|
||||
<title>Log</title>
|
||||
<path d="M9.516 14.016c2.484 0 4.5-2.016 4.5-4.5s-2.016-4.5-4.5-4.5-4.5 2.016-4.5 4.5 2.016 4.5 4.5 4.5zM15.516 14.016l4.969 4.969-1.5 1.5-4.969-4.969v-0.797l-0.281-0.281c-1.125 0.984-2.625 1.547-4.219 1.547-3.609 0-6.516-2.859-6.516-6.469s2.906-6.516 6.516-6.516 6.469 2.906 6.469 6.516c0 1.594-0.563 3.094-1.547 4.219l0.281 0.281h0.797z"></path>
|
||||
</symbol>
|
||||
<symbol id="server" viewBox="0 0 24 28">
|
||||
<title>Server</title>
|
||||
<path d="M12 12c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 24c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 18c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 0c6.625 0 12 1.797 12 4v2c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2c0-2.203 5.375-4 12-4z"></path>
|
||||
</symbol>
|
||||
<symbol id="study-list" viewBox="0 0 24 24">
|
||||
<title>Study List</title>
|
||||
<path d="M9 5.016h12v3.984h-12v-3.984zM9 18.984v-3.984h12v3.984h-12zM9 14.016v-4.031h12v4.031h-12zM3.984 9v-3.984h4.031v3.984h-4.031zM3.984 18.984v-3.984h4.031v3.984h-4.031zM3.984 14.016v-4.031h4.031v4.031h-4.031z"></path>
|
||||
</symbol>
|
||||
<symbol id="logout" viewBox="0 0 24 28">
|
||||
<title>Logout</title>
|
||||
<path d="M24 14c0 6.609-5.391 12-12 12s-12-5.391-12-12c0-3.797 1.75-7.297 4.797-9.578 0.891-0.672 2.141-0.5 2.797 0.391 0.672 0.875 0.484 2.141-0.391 2.797-2.031 1.531-3.203 3.859-3.203 6.391 0 4.406 3.594 8 8 8s8-3.594 8-8c0-2.531-1.172-4.859-3.203-6.391-0.875-0.656-1.062-1.922-0.391-2.797 0.656-0.891 1.922-1.062 2.797-0.391 3.047 2.281 4.797 5.781 4.797 9.578zM14 2v10c0 1.094-0.906 2-2 2s-2-0.906-2-2v-10c0-1.094 0.906-2 2-2s2 0.906 2 2z"></path>
|
||||
</symbol>
|
||||
<symbol id="password" viewBox="0 0 18 28">
|
||||
<title>Password</title>
|
||||
<path d="M5 12h8v-3c0-2.203-1.797-4-4-4s-4 1.797-4 4v3zM18 13.5v9c0 0.828-0.672 1.5-1.5 1.5h-15c-0.828 0-1.5-0.672-1.5-1.5v-9c0-0.828 0.672-1.5 1.5-1.5h0.5v-3c0-3.844 3.156-7 7-7s7 3.156 7 7v3h0.5c0.828 0 1.5 0.672 1.5 1.5z"></path>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@ -29,6 +29,7 @@ Package.onUse(function(api) {
|
||||
|
||||
const assets = [
|
||||
'assets/icons.svg',
|
||||
'assets/user-menu-icons.svg',
|
||||
'assets/fonts/Roboto-Black-latin-ext.woff',
|
||||
'assets/fonts/Roboto-Black-latin-ext.woff2',
|
||||
'assets/fonts/Roboto-Black-latin.woff',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user