Compare commits
1 Commits
fix/EE-684
...
feat/EE-88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64b91d5258 |
@@ -139,6 +139,7 @@ func (service *Service) CreateUser(user *portainer.User) error {
|
||||
id, _ := bucket.NextSequence()
|
||||
user.ID = portainer.UserID(id)
|
||||
user.Username = strings.ToLower(user.Username)
|
||||
user.HasAuthenticatedOnce = false
|
||||
|
||||
data, err := internal.MarshalObject(user)
|
||||
if err != nil {
|
||||
|
||||
@@ -131,7 +131,16 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
return handler.persistAndWriteToken(w, composeTokenData(user))
|
||||
|
||||
tokenData := composeTokenData(user)
|
||||
|
||||
user.HasAuthenticatedOnce = true
|
||||
err := handler.DataStore.User().UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time) *httperror.HandlerError {
|
||||
@@ -139,6 +148,14 @@ func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portaine
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
|
||||
}
|
||||
|
||||
user.HasAuthenticatedOnce = true
|
||||
|
||||
err = handler.DataStore.User().UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &authenticateResponse{JWT: token})
|
||||
}
|
||||
|
||||
@@ -210,8 +227,9 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
|
||||
|
||||
func composeTokenData(user *portainer.User) *portainer.TokenData {
|
||||
return &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
HasAuthenticatedOnce: user.HasAuthenticatedOnce,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,10 @@ type Service struct {
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
HasAuthenticatedOnce bool `json:"hasAuthenticatedOnce"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
@@ -94,9 +95,10 @@ func (service *Service) generateSignedToken(data *portainer.TokenData, expiryTim
|
||||
expireToken = expiryTime.Unix()
|
||||
}
|
||||
cl := claims{
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
HasAuthenticatedOnce: bool(data.HasAuthenticatedOnce),
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expireToken,
|
||||
},
|
||||
|
||||
@@ -14,9 +14,10 @@ func TestGenerateSignedToken(t *testing.T) {
|
||||
assert.NoError(t, err, "failed to create a copy of service")
|
||||
|
||||
token := &portainer.TokenData{
|
||||
Username: "Joe",
|
||||
ID: 1,
|
||||
Role: 1,
|
||||
Username: "Joe",
|
||||
ID: 1,
|
||||
Role: 1,
|
||||
HasAuthenticatedOnce: true,
|
||||
}
|
||||
expirtationTime := time.Now().Add(1 * time.Hour)
|
||||
|
||||
@@ -34,5 +35,6 @@ func TestGenerateSignedToken(t *testing.T) {
|
||||
assert.Equal(t, token.Username, tokenClaims.Username)
|
||||
assert.Equal(t, int(token.ID), tokenClaims.UserID)
|
||||
assert.Equal(t, int(token.Role), tokenClaims.Role)
|
||||
assert.Equal(t, bool(token.HasAuthenticatedOnce), tokenClaims.HasAuthenticatedOnce)
|
||||
assert.Equal(t, expirtationTime.Unix(), tokenClaims.ExpiresAt)
|
||||
}
|
||||
|
||||
@@ -927,9 +927,10 @@ type (
|
||||
|
||||
// TokenData represents the data embedded in a JWT token
|
||||
TokenData struct {
|
||||
ID UserID
|
||||
Username string
|
||||
Role UserRole
|
||||
ID UserID
|
||||
Username string
|
||||
Role UserRole
|
||||
HasAuthenticatedOnce bool
|
||||
}
|
||||
|
||||
// TunnelDetails represents information associated to a tunnel
|
||||
@@ -953,7 +954,8 @@ type (
|
||||
Username string `json:"Username" example:"bob"`
|
||||
Password string `json:"Password,omitempty" example:"passwd"`
|
||||
// User role (1 for administrator account and 2 for regular account)
|
||||
Role UserRole `json:"Role" example:"1"`
|
||||
Role UserRole `json:"Role" example:"1"`
|
||||
HasAuthenticatedOnce bool `json:"HasAuthenticatedOnce" example:"true"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 25
|
||||
|
||||
@@ -93,11 +93,6 @@ a[ng-click] {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.portainer-disabled-link {
|
||||
color: gray;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.portainer-disabled-datatable {
|
||||
color: gray;
|
||||
}
|
||||
@@ -126,6 +121,10 @@ a[ng-click] {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.color-portainer-black {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.fa.green-icon {
|
||||
color: #23ae89;
|
||||
}
|
||||
@@ -151,6 +150,10 @@ a[ng-click] {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
|
||||
.text-blue {
|
||||
color: #337ab7;
|
||||
}
|
||||
|
||||
.widget .widget-body table tbody .image-tag {
|
||||
font-size: 90% !important;
|
||||
margin-right: 5px;
|
||||
|
||||
BIN
app/assets/images/help-links/admin.png
Normal file
BIN
app/assets/images/help-links/admin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
app/assets/images/help-links/developer.png
Normal file
BIN
app/assets/images/help-links/developer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
app/assets/images/help-links/platform_engineer.png
Normal file
BIN
app/assets/images/help-links/platform_engineer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
app/assets/images/help-links/youtube.png
Normal file
BIN
app/assets/images/help-links/youtube.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -163,6 +163,20 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
|
||||
},
|
||||
};
|
||||
|
||||
const helpAndSupport = {
|
||||
name: 'portainer.help',
|
||||
url: '/help',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'helpAndSupportView',
|
||||
},
|
||||
},
|
||||
params: {
|
||||
firstLogin: false,
|
||||
initFirstEndpoint: false,
|
||||
},
|
||||
};
|
||||
|
||||
var endpointCreation = {
|
||||
name: 'portainer.endpoints.new',
|
||||
url: '/new',
|
||||
@@ -407,6 +421,7 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
|
||||
$stateRegistryProvider.register(group);
|
||||
$stateRegistryProvider.register(groupAccess);
|
||||
$stateRegistryProvider.register(groupCreation);
|
||||
$stateRegistryProvider.register(helpAndSupport);
|
||||
$stateRegistryProvider.register(home);
|
||||
$stateRegistryProvider.register(init);
|
||||
$stateRegistryProvider.register(initEndpoint);
|
||||
|
||||
@@ -88,6 +88,7 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
user.username = tokenPayload.username;
|
||||
user.ID = tokenPayload.id;
|
||||
user.role = tokenPayload.role;
|
||||
user.firstLogin = !(tokenPayload.hasAuthenticatedOnce ? tokenPayload.hasAuthenticatedOnce : false);
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
|
||||
@@ -120,8 +120,13 @@ class AuthenticationController {
|
||||
try {
|
||||
const endpoints = await this.EndpointService.endpoints(0, 1);
|
||||
const isAdmin = this.Authentication.isAdmin();
|
||||
const firstLogin = this.Authentication.getUserDetails().firstLogin;
|
||||
|
||||
if (endpoints.value.length === 0 && isAdmin) {
|
||||
const shouldInitFirstEndpoint = isAdmin && endpoints.value.length === 0;
|
||||
|
||||
if (firstLogin) {
|
||||
return this.$state.go('portainer.help', { firstLogin: true, initFirstEndpoint: shouldInitFirstEndpoint });
|
||||
} else if (shouldInitFirstEndpoint) {
|
||||
return this.$state.go('portainer.init.endpoint');
|
||||
} else {
|
||||
return this.$state.go('portainer.home');
|
||||
@@ -150,8 +155,8 @@ class AuthenticationController {
|
||||
|
||||
async postLoginSteps() {
|
||||
await this.StateManager.initialize();
|
||||
await this.checkForEndpointsAsync();
|
||||
await this.checkForLatestVersionAsync();
|
||||
await this.checkForEndpointsAsync();
|
||||
}
|
||||
/**
|
||||
* END POST LOGIN STEPS SECTION
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import './por-help-image-link.css';
|
||||
|
||||
export const helpImageLink = {
|
||||
templateUrl: './por-help-image-link.html',
|
||||
bindings: {
|
||||
data: '<',
|
||||
copyLink: '<',
|
||||
onLinkClicked: '<',
|
||||
},
|
||||
};
|
||||
|
||||
angular.module('portainer.app').component('porHelpImageLink', helpImageLink);
|
||||
@@ -0,0 +1,21 @@
|
||||
.portainer-image-link {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.portainer-image-link img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
|
||||
}
|
||||
|
||||
.portainer-image-link i {
|
||||
position: absolute;
|
||||
bottom: 1%;
|
||||
right: 1%;
|
||||
}
|
||||
|
||||
.portainer-disabled-link {
|
||||
color: gray;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="col-lg-2 col-md-3 col-sm-4 col-xs-6" style="text-align: center;">
|
||||
<div class="portainer-image-link">
|
||||
<a id="{{ $ctrl.data.linkId }}" ng-href="{{ $ctrl.data.redirectUrl }}" target="_blank" ng-click="$ctrl.onLinkClicked($ctrl.data.matomoAction)">
|
||||
<img ng-src="{{ $ctrl.data.image }}" alt="{{ $ctrl.data.imageAltText }}" />
|
||||
<i class="fa fa-external-link-alt color-portainer-black"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<label>{{ $ctrl.data.description }}</label>
|
||||
<div>
|
||||
<span id="copy{{ $ctrl.data.linkId }}" class="btn btn-sm text-blue" ng-click="$ctrl.copyLink($ctrl.data.redirectUrl, $ctrl.data.linkId)">
|
||||
<i class="fa fa-copy space-right" aria-hidden="true"></i>Copy link</span
|
||||
>
|
||||
<span id="copy{{ $ctrl.data.linkId }}Notification" style="margin-left: 7px; display: none; color: #23ae89;"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
const MatomoCategories = Object.freeze({
|
||||
ADMIN: 'Admin',
|
||||
STANDARD: 'Non-admin',
|
||||
GETTING_STARTED: 'Getting Started Page',
|
||||
});
|
||||
// TO REMOVE
|
||||
void MatomoCategories;
|
||||
|
||||
const MatomoActions = Object.freeze({
|
||||
ADMIN: 'admin',
|
||||
ENGINEER: 'platform',
|
||||
DEVELOPER: 'dev',
|
||||
YOUTUBE: 'portainertube',
|
||||
ENTER: 'getting-started-help&support',
|
||||
SKIP: 'getting-started-skip',
|
||||
DONE: 'getting-started-done',
|
||||
});
|
||||
|
||||
const links = {
|
||||
admin: {
|
||||
linkId: 'admin',
|
||||
image: require('@/assets/images/help-links/admin.png'),
|
||||
imageAltText: 'Portainer admin setup',
|
||||
redirectUrl: 'https://portainer.io/inapp/ceadmin',
|
||||
description: 'Admin - Setting up Portainer',
|
||||
matomoAction: MatomoActions.ADMIN,
|
||||
},
|
||||
engineer: {
|
||||
linkId: 'engineer',
|
||||
image: require('@/assets/images/help-links/platform_engineer.png'),
|
||||
imageAltText: 'Platform engineer',
|
||||
redirectUrl: 'https://portainer.io/inapp/ceplatform',
|
||||
description: 'A Platform Engineer Experience',
|
||||
matomoAction: MatomoActions.ENGINEER,
|
||||
},
|
||||
developer: {
|
||||
linkId: 'developer',
|
||||
image: require('@/assets/images/help-links/developer.png'),
|
||||
imageAltText: 'Developer',
|
||||
redirectUrl: 'https://portainer.io/inapp/cedev',
|
||||
description: 'A Developer Experience',
|
||||
matomoAction: MatomoActions.DEVELOPER,
|
||||
},
|
||||
youtube: {
|
||||
linkId: 'youtube',
|
||||
image: require('@/assets/images/help-links/youtube.png'),
|
||||
imageAltText: 'Youtube',
|
||||
redirectUrl: 'https://www.youtube.com/c/portainerio',
|
||||
description: 'Portainer.io Youtube Channel',
|
||||
matomoAction: MatomoActions.YOUTUBE,
|
||||
},
|
||||
};
|
||||
|
||||
class HelpAndSupportController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, clipboard, Authentication) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.clipboard = clipboard;
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.copyLink = this.copyLink.bind(this);
|
||||
this.onLinkClicked = this.onLinkClicked.bind(this);
|
||||
|
||||
this.links = links;
|
||||
this.state = {
|
||||
firstLogin: false,
|
||||
linkClicked: false,
|
||||
};
|
||||
}
|
||||
|
||||
copyLink(link, linkId) {
|
||||
this.clipboard.copyText(link);
|
||||
$(`#copy${linkId}Notification`).show().fadeOut(2500);
|
||||
}
|
||||
|
||||
onLinkClicked(matomoAction) {
|
||||
void matomoAction;
|
||||
|
||||
this.state.linkClicked = true;
|
||||
// send matomo event
|
||||
}
|
||||
|
||||
closeHelpView() {
|
||||
// send matomo event
|
||||
const initFirstEndpoint = this.$transition$.params().initFirstEndpoint;
|
||||
|
||||
if (this.state.firstLogin && this.isAdmin && initFirstEndpoint) {
|
||||
return this.$state.go('portainer.init.endpoint');
|
||||
}
|
||||
return this.$state.go('portainer.home');
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
this.state.firstLogin = this.$transition$.params().firstLogin;
|
||||
// send matomo event
|
||||
}
|
||||
}
|
||||
|
||||
export default HelpAndSupportController;
|
||||
53
app/portainer/views/help-and-support/help-and-support.html
Normal file
53
app/portainer/views/help-and-support/help-and-support.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Help & Support"></rd-header-title>
|
||||
<rd-header-content>Video walkthroughs</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="$ctrl.form">
|
||||
<h2 class="col-sm-12 form-section-title color-portainer-black">
|
||||
Getting started
|
||||
</h2>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted">
|
||||
Select an area of interest for a video walkthrough
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 class="col-sm-12 form-section-title color-portainer-black" ng-if="$ctrl.isAdmin">
|
||||
Setting up Portainer
|
||||
</h4>
|
||||
<div class="form-group" ng-if="$ctrl.isAdmin">
|
||||
<por-help-image-link data="$ctrl.links.admin" copy-link="$ctrl.copyLink" on-link-clicked="($ctrl.onLinkClicked)"> </por-help-image-link>
|
||||
</div>
|
||||
|
||||
<h4 class="col-sm-12 form-section-title color-portainer-black">
|
||||
Using Portainer
|
||||
</h4>
|
||||
<div class="form-group">
|
||||
<por-help-image-link data="$ctrl.links.engineer" copy-link="$ctrl.copyLink" on-link-clicked="($ctrl.onLinkClicked)"> </por-help-image-link>
|
||||
<por-help-image-link data="$ctrl.links.developer" copy-link="$ctrl.copyLink" on-link-clicked="($ctrl.onLinkClicked)"> </por-help-image-link>
|
||||
</div>
|
||||
|
||||
<h4 class="col-sm-12 form-section-title color-portainer-black">
|
||||
Learn more...
|
||||
</h4>
|
||||
<div class="form-group">
|
||||
<por-help-image-link data="$ctrl.links.youtube" copy-link="$ctrl.copyLink" on-link-clicked="($ctrl.onLinkClicked)"> </por-help-image-link>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.state.firstLogin">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.closeHelpView()">
|
||||
<span>{{ $ctrl.state.linkClicked ? 'Done' : 'Skip' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
12
app/portainer/views/help-and-support/index.js
Normal file
12
app/portainer/views/help-and-support/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import angular from 'angular';
|
||||
import controller from './help-and-support.controller';
|
||||
|
||||
export const helpAndSupport = {
|
||||
templateUrl: './help-and-support.html',
|
||||
controller,
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
};
|
||||
|
||||
angular.module('portainer.app').component('helpAndSupportView', helpAndSupport);
|
||||
@@ -49,10 +49,17 @@ angular.module('portainer.app').controller('InitAdminController', [
|
||||
return EndpointService.endpoints(0, 100);
|
||||
})
|
||||
.then(function success(data) {
|
||||
if (data.value.length === 0) {
|
||||
$state.go('portainer.init.endpoint');
|
||||
const firstLogin = Authentication.getUserDetails().firstLogin;
|
||||
const initFirstEndpoint = data.value.length === 0;
|
||||
|
||||
if (firstLogin) {
|
||||
return $state.go('portainer.help', { firstLogin: true, initFirstEndpoint: initFirstEndpoint });
|
||||
}
|
||||
|
||||
if (initFirstEndpoint) {
|
||||
return $state.go('portainer.init.endpoint');
|
||||
} else {
|
||||
$state.go('portainer.home');
|
||||
return $state.go('portainer.home');
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
||||
@@ -194,16 +194,21 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer-content">
|
||||
<div class="update-notification" ng-if="applicationState.application.versionStatus.UpdateAvailable">
|
||||
<a target="_blank" href="https://github.com/portainer/portainer/releases/tag/{{ applicationState.application.versionStatus.LatestVersion }}" style="color: #091e5d;">
|
||||
<i class="fa-lg fas fa-cloud-download-alt" style="margin-right: 2px;"></i> A new version is available
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<img src="~@/assets/images/logo_small.png" class="img-responsive logo" alt="Portainer" />
|
||||
<span class="version">{{ uiVersion }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-footer-content">
|
||||
<ul class="sidebar">
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="portainer.help" ui-sref-active="active" style="text-indent: 0px;">Help & support</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="update-notification" ng-if="applicationState.application.versionStatus.UpdateAvailable">
|
||||
<a target="_blank" href="https://github.com/portainer/portainer/releases/tag/{{ applicationState.application.versionStatus.LatestVersion }}" style="color: #091e5d;">
|
||||
<i class="fa-lg fas fa-cloud-download-alt" style="margin-right: 2px;"></i> A new version is available
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<img src="~@/assets/images/logo_small.png" class="img-responsive logo" alt="Portainer" />
|
||||
<span class="version">{{ uiVersion }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user