External apps authentication
parent
6b837d2067
commit
f52a4ba788
|
@ -10,6 +10,7 @@
|
||||||
- Ability to choose a theme (thanks to [@astagi](https://github.com/astagi))
|
- Ability to choose a theme (thanks to [@astagi](https://github.com/astagi))
|
||||||
- Inline viewing of image attachments (thanks to [@brettp](https://github.com/brettp)).
|
- Inline viewing of image attachments (thanks to [@brettp](https://github.com/brettp)).
|
||||||
- Autocomplete for usernames, user stories, tasks, issues, and wiki pages in text areas (thanks to [@brettp](https://github.com/brettp)).
|
- Autocomplete for usernames, user stories, tasks, issues, and wiki pages in text areas (thanks to [@brettp](https://github.com/brettp)).
|
||||||
|
- Support authentication via Application Tokens
|
||||||
- i18n.
|
- i18n.
|
||||||
- Add polish (pl) translation.
|
- Add polish (pl) translation.
|
||||||
- Add portuguese (Brazil) (pt_BR) translation.
|
- Add portuguese (Brazil) (pt_BR) translation.
|
||||||
|
|
|
@ -68,7 +68,6 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
},
|
},
|
||||||
title: "HOME.PAGE_TITLE",
|
title: "HOME.PAGE_TITLE",
|
||||||
description: "HOME.PAGE_DESCRIPTION",
|
description: "HOME.PAGE_DESCRIPTION",
|
||||||
loader: true
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -334,22 +333,25 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
$routeProvider.when("/login",
|
$routeProvider.when("/login",
|
||||||
{
|
{
|
||||||
templateUrl: "auth/login.html",
|
templateUrl: "auth/login.html",
|
||||||
title: "LOGIN.PAGE_TITLE"
|
title: "LOGIN.PAGE_TITLE",
|
||||||
description: "LOGIN.PAGE_DESCRIPTION"
|
description: "LOGIN.PAGE_DESCRIPTION",
|
||||||
|
disableHeader: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
$routeProvider.when("/register",
|
$routeProvider.when("/register",
|
||||||
{
|
{
|
||||||
templateUrl: "auth/register.html",
|
templateUrl: "auth/register.html",
|
||||||
title: "REGISTER.PAGE_TITLE",
|
title: "REGISTER.PAGE_TITLE",
|
||||||
description: "REGISTER.PAGE_DESCRIPTION"
|
description: "REGISTER.PAGE_DESCRIPTION",
|
||||||
|
disableHeader: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
$routeProvider.when("/forgot-password",
|
$routeProvider.when("/forgot-password",
|
||||||
{
|
{
|
||||||
templateUrl: "auth/forgot-password.html",
|
templateUrl: "auth/forgot-password.html",
|
||||||
title: "FORGOT_PASSWORD.PAGE_TITLE",
|
title: "FORGOT_PASSWORD.PAGE_TITLE",
|
||||||
description: "FORGOT_PASSWORD.PAGE_DESCRIPTION"
|
description: "FORGOT_PASSWORD.PAGE_DESCRIPTION",
|
||||||
|
disableHeader: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
$routeProvider.when("/change-password",
|
$routeProvider.when("/change-password",
|
||||||
|
@ -357,6 +359,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
templateUrl: "auth/change-password-from-recovery.html",
|
templateUrl: "auth/change-password-from-recovery.html",
|
||||||
title: "CHANGE_PASSWORD.PAGE_TITLE",
|
title: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||||
description: "CHANGE_PASSWORD.PAGE_TITLE",
|
description: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||||
|
disableHeader: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
$routeProvider.when("/change-password/:token",
|
$routeProvider.when("/change-password/:token",
|
||||||
|
@ -364,13 +367,29 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
templateUrl: "auth/change-password-from-recovery.html",
|
templateUrl: "auth/change-password-from-recovery.html",
|
||||||
title: "CHANGE_PASSWORD.PAGE_TITLE",
|
title: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||||
description: "CHANGE_PASSWORD.PAGE_TITLE",
|
description: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||||
|
disableHeader: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
$routeProvider.when("/invitation/:token",
|
$routeProvider.when("/invitation/:token",
|
||||||
{
|
{
|
||||||
templateUrl: "auth/invitation.html",
|
templateUrl: "auth/invitation.html",
|
||||||
title: "INVITATION.PAGE_TITLE",
|
title: "INVITATION.PAGE_TITLE",
|
||||||
description: "INVITATION.PAGE_DESCRIPTION"
|
description: "INVITATION.PAGE_DESCRIPTION",
|
||||||
|
disableHeader: true,
|
||||||
|
access: {
|
||||||
|
requiresLogin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
$routeProvider.when("/external-apps",
|
||||||
|
{
|
||||||
|
templateUrl: "external-apps/external-app.html",
|
||||||
|
title: "EXTERNAL_APP.PAGE_TITLE",
|
||||||
|
description: "EXTERNAL_APP.PAGE_DESCRIPTION",
|
||||||
|
controller: "ExternalApp",
|
||||||
|
controllerAs: "vm",
|
||||||
|
disableHeader: true,
|
||||||
|
mobileViewport: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -411,8 +430,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
$location.path($navUrls.resolve("error"))
|
$location.path($navUrls.resolve("error"))
|
||||||
$location.replace()
|
$location.replace()
|
||||||
else if response.status == 401
|
else if response.status == 401
|
||||||
nextPath = $location.path()
|
nextUrl = encodeURIComponent($location.url())
|
||||||
$location.url($navUrls.resolve("login")).search("next=#{nextPath}")
|
$location.url($navUrls.resolve("login")).search("next=#{nextUrl}")
|
||||||
|
|
||||||
return $q.reject(response)
|
return $q.reject(response)
|
||||||
|
|
||||||
|
@ -537,7 +556,7 @@ i18nInit = (lang, $translate) ->
|
||||||
checksley.updateMessages('default', messages)
|
checksley.updateMessages('default', messages)
|
||||||
|
|
||||||
|
|
||||||
init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, projectService, loaderService) ->
|
init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, projectService, loaderService, navigationBarService) ->
|
||||||
$log.debug("Initialize application")
|
$log.debug("Initialize application")
|
||||||
|
|
||||||
# Taiga Plugins
|
# Taiga Plugins
|
||||||
|
@ -590,6 +609,15 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na
|
||||||
description = $translate.instant(next.description or "")
|
description = $translate.instant(next.description or "")
|
||||||
appMetaService.setAll(title, description)
|
appMetaService.setAll(title, description)
|
||||||
|
|
||||||
|
if next.mobileViewport
|
||||||
|
appMetaService.addMobileViewport()
|
||||||
|
else
|
||||||
|
appMetaService.removeMobileViewport()
|
||||||
|
|
||||||
|
if next.disableHeader
|
||||||
|
navigationBarService.disableHeader()
|
||||||
|
else
|
||||||
|
navigationBarService.enableHeader()
|
||||||
|
|
||||||
modules = [
|
modules = [
|
||||||
# Main Global Modules
|
# Main Global Modules
|
||||||
|
@ -625,6 +653,7 @@ modules = [
|
||||||
"taigaProfile",
|
"taigaProfile",
|
||||||
"taigaHome",
|
"taigaHome",
|
||||||
"taigaUserTimeline",
|
"taigaUserTimeline",
|
||||||
|
"taigaExternalApps",
|
||||||
|
|
||||||
# template cache
|
# template cache
|
||||||
"templates",
|
"templates",
|
||||||
|
@ -664,5 +693,6 @@ module.run([
|
||||||
"tgAppMetaService",
|
"tgAppMetaService",
|
||||||
"tgProjectService",
|
"tgProjectService",
|
||||||
"tgLoader",
|
"tgLoader",
|
||||||
|
"tgNavigationBarService"
|
||||||
init
|
init
|
||||||
])
|
])
|
||||||
|
|
|
@ -221,12 +221,12 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
onSuccess = (response) ->
|
onSuccess = (response) ->
|
||||||
if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login")
|
if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login")
|
||||||
nextUrl = $routeParams['next']
|
nextUrl = decodeURIComponent($routeParams['next'])
|
||||||
else
|
else
|
||||||
nextUrl = $navUrls.resolve("home")
|
nextUrl = $navUrls.resolve("home")
|
||||||
|
|
||||||
$events.setupConnection()
|
$events.setupConnection()
|
||||||
$location.path(nextUrl)
|
$location.url(nextUrl)
|
||||||
|
|
||||||
onError = (response) ->
|
onError = (response) ->
|
||||||
$confirm.notify("light-error", $translate.instant("LOGIN_FORM.ERROR_AUTH_INCORRECT"))
|
$confirm.notify("light-error", $translate.instant("LOGIN_FORM.ERROR_AUTH_INCORRECT"))
|
||||||
|
|
|
@ -149,6 +149,10 @@ urls = {
|
||||||
|
|
||||||
# locales
|
# locales
|
||||||
"locales": "/locales"
|
"locales": "/locales"
|
||||||
|
|
||||||
|
# Application tokens
|
||||||
|
"applications": "/applications"
|
||||||
|
"application-tokens": "/application-tokens"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize api urls service
|
# Initialize api urls service
|
||||||
|
|
|
@ -1268,5 +1268,13 @@
|
||||||
"BLOCKED": "{{username}} has blocked {{obj_name}}",
|
"BLOCKED": "{{username}} has blocked {{obj_name}}",
|
||||||
"UNBLOCKED": "{{username}} has unblocked {{obj_name}}",
|
"UNBLOCKED": "{{username}} has unblocked {{obj_name}}",
|
||||||
"NEW_USER": "{{username}} has joined Taiga"
|
"NEW_USER": "{{username}} has joined Taiga"
|
||||||
|
},
|
||||||
|
"EXTERNAL_APP": {
|
||||||
|
"PAGE_TITLE": "An external app requires authentication",
|
||||||
|
"PAGE_DESCRIPTION": "An external app requires authentication",
|
||||||
|
"AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?",
|
||||||
|
"LOGIN_WITH_ANOTHER_USER": "Login with another user",
|
||||||
|
"AUTHORIZE_APP": "Authorize app",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
taiga = @.taiga
|
||||||
|
|
||||||
|
class ExternalAppController extends taiga.Controller
|
||||||
|
@.$inject = [
|
||||||
|
"$routeParams",
|
||||||
|
"tgExternalAppsService",
|
||||||
|
"$window",
|
||||||
|
"tgCurrentUserService",
|
||||||
|
"$location",
|
||||||
|
"$tgNavUrls",
|
||||||
|
"tgXhrErrorService",
|
||||||
|
"tgLoader"
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@routeParams, @externalAppsService, @window, @currentUserService,
|
||||||
|
@location, @navUrls, @xhrError, @loader) ->
|
||||||
|
|
||||||
|
@loader.start(false)
|
||||||
|
@._applicationId = @routeParams.application
|
||||||
|
@._state = @routeParams.state
|
||||||
|
@._getApplicationToken()
|
||||||
|
@._user = @currentUserService.getUser()
|
||||||
|
@._application = null
|
||||||
|
nextUrl = encodeURIComponent(@location.url())
|
||||||
|
loginUrl = @navUrls.resolve("login")
|
||||||
|
@.loginWithAnotherUserUrl = "#{loginUrl}?next=#{nextUrl}"
|
||||||
|
|
||||||
|
taiga.defineImmutableProperty @, "user", () => @._user
|
||||||
|
taiga.defineImmutableProperty @, "application", () => @._application
|
||||||
|
|
||||||
|
_redirect: (applicationToken) =>
|
||||||
|
nextUrl = applicationToken.get("next_url")
|
||||||
|
@window.open(nextUrl, "_self")
|
||||||
|
|
||||||
|
_getApplicationToken: =>
|
||||||
|
return @externalAppsService.getApplicationToken(@._applicationId, @._state)
|
||||||
|
.then (data) =>
|
||||||
|
@._application = data.get("application")
|
||||||
|
if data.get("auth_code")
|
||||||
|
@._redirect(data)
|
||||||
|
else
|
||||||
|
@loader.pageLoaded()
|
||||||
|
|
||||||
|
.catch (xhr) =>
|
||||||
|
return @xhrError.response(xhr)
|
||||||
|
|
||||||
|
cancel: () ->
|
||||||
|
@window.history.back()
|
||||||
|
|
||||||
|
createApplicationToken: =>
|
||||||
|
return @externalAppsService.authorizeApplicationToken(@._applicationId, @._state)
|
||||||
|
.then (data) =>
|
||||||
|
@._redirect(data)
|
||||||
|
.catch (xhr) =>
|
||||||
|
return @xhrError.response(xhr)
|
||||||
|
|
||||||
|
|
||||||
|
angular.module("taigaExternalApps").controller("ExternalApp", ExternalAppController)
|
|
@ -0,0 +1,164 @@
|
||||||
|
describe "ExternalAppController", ->
|
||||||
|
provide = null
|
||||||
|
$controller = null
|
||||||
|
$rootScope = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
_inject = (callback) ->
|
||||||
|
inject (_$controller_, _$rootScope_) ->
|
||||||
|
$rootScope = _$rootScope_
|
||||||
|
$controller = _$controller_
|
||||||
|
|
||||||
|
_mockRouteParams = () ->
|
||||||
|
mocks.routeParams = {}
|
||||||
|
provide.value "$routeParams", mocks.routeParams
|
||||||
|
|
||||||
|
_mockTgExternalAppsService = () ->
|
||||||
|
mocks.tgExternalAppsService = {
|
||||||
|
getApplicationToken: sinon.stub()
|
||||||
|
authorizeApplicationToken: sinon.stub()
|
||||||
|
}
|
||||||
|
provide.value "tgExternalAppsService", mocks.tgExternalAppsService
|
||||||
|
|
||||||
|
_mockWindow = () ->
|
||||||
|
mocks.window = {
|
||||||
|
open: sinon.stub()
|
||||||
|
history: {
|
||||||
|
back: sinon.stub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
provide.value "$window", mocks.window
|
||||||
|
|
||||||
|
_mockTgCurrentUserService = () ->
|
||||||
|
mocks.tgCurrentUserService = {
|
||||||
|
getUser: sinon.stub()
|
||||||
|
}
|
||||||
|
provide.value "tgCurrentUserService", mocks.tgCurrentUserService
|
||||||
|
|
||||||
|
_mockLocation = () ->
|
||||||
|
mocks.location = {
|
||||||
|
url: sinon.stub()
|
||||||
|
}
|
||||||
|
provide.value "$location", mocks.location
|
||||||
|
|
||||||
|
_mockTgNavUrls = () ->
|
||||||
|
mocks.tgNavUrls = {
|
||||||
|
resolve: sinon.stub()
|
||||||
|
}
|
||||||
|
provide.value "$tgNavUrls", mocks.tgNavUrls
|
||||||
|
|
||||||
|
_mockTgXhrErrorService = () ->
|
||||||
|
mocks.tgXhrErrorService = {
|
||||||
|
response: sinon.spy(),
|
||||||
|
notFound: sinon.spy()
|
||||||
|
}
|
||||||
|
provide.value "tgXhrErrorService", mocks.tgXhrErrorService
|
||||||
|
|
||||||
|
_mockTgLoader = () ->
|
||||||
|
mocks.tgLoader = {
|
||||||
|
start: sinon.stub(),
|
||||||
|
pageLoaded: sinon.stub()
|
||||||
|
}
|
||||||
|
provide.value "tgLoader", mocks.tgLoader
|
||||||
|
|
||||||
|
_mocks = () ->
|
||||||
|
module ($provide) ->
|
||||||
|
provide = $provide
|
||||||
|
_mockRouteParams()
|
||||||
|
_mockTgExternalAppsService()
|
||||||
|
_mockWindow()
|
||||||
|
_mockTgCurrentUserService()
|
||||||
|
_mockLocation()
|
||||||
|
_mockTgNavUrls()
|
||||||
|
_mockTgXhrErrorService()
|
||||||
|
_mockTgLoader()
|
||||||
|
return null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaExternalApps"
|
||||||
|
_mocks()
|
||||||
|
_inject()
|
||||||
|
|
||||||
|
it "not existing application", (done) ->
|
||||||
|
$scope = $rootScope.$new()
|
||||||
|
|
||||||
|
mocks.routeParams.application = 6
|
||||||
|
mocks.routeParams.state = "testing-state"
|
||||||
|
|
||||||
|
xhr = {
|
||||||
|
status: 404
|
||||||
|
}
|
||||||
|
|
||||||
|
mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().reject(xhr)
|
||||||
|
|
||||||
|
ctrl = $controller("ExternalApp")
|
||||||
|
|
||||||
|
setTimeout ( ->
|
||||||
|
expect(mocks.tgLoader.start.withArgs(false)).to.be.calledOnce
|
||||||
|
expect(mocks.tgXhrErrorService.response.withArgs(xhr)).to.be.calledOnce
|
||||||
|
done()
|
||||||
|
)
|
||||||
|
|
||||||
|
it "existing application and existing token, automatically redirecting to next url", (done) ->
|
||||||
|
$scope = $rootScope.$new()
|
||||||
|
|
||||||
|
mocks.routeParams.application = 6
|
||||||
|
mocks.routeParams.state = "testing-state"
|
||||||
|
|
||||||
|
applicationToken = Immutable.fromJS({
|
||||||
|
auth_code: "testing-auth-code"
|
||||||
|
next_url: "http://next.url"
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken)
|
||||||
|
|
||||||
|
ctrl = $controller("ExternalApp")
|
||||||
|
|
||||||
|
setTimeout ( ->
|
||||||
|
expect(mocks.tgLoader.start.withArgs(false)).to.be.calledOnce
|
||||||
|
expect(mocks.window.open.callCount).to.be.equal(1)
|
||||||
|
expect(mocks.window.open.calledWith("http://next.url")).to.be.true
|
||||||
|
done()
|
||||||
|
)
|
||||||
|
|
||||||
|
it "existing application and creating new token", (done) ->
|
||||||
|
$scope = $rootScope.$new()
|
||||||
|
|
||||||
|
mocks.routeParams.application = 6
|
||||||
|
mocks.routeParams.state = "testing-state"
|
||||||
|
|
||||||
|
applicationToken = Immutable.fromJS({})
|
||||||
|
mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken)
|
||||||
|
|
||||||
|
ctrl = $controller("ExternalApp")
|
||||||
|
|
||||||
|
applicationToken = Immutable.fromJS({
|
||||||
|
next_url: "http://next.url"
|
||||||
|
auth_code: "testing-auth-code"
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.tgExternalAppsService.authorizeApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken)
|
||||||
|
|
||||||
|
ctrl.createApplicationToken()
|
||||||
|
|
||||||
|
setTimeout ( ->
|
||||||
|
expect(mocks.tgLoader.start.withArgs(false)).to.be.calledOnce
|
||||||
|
expect(mocks.tgLoader.pageLoaded).to.be.calledOnce
|
||||||
|
expect(mocks.window.open.callCount).to.be.equal(1)
|
||||||
|
expect(mocks.window.open.calledWith("http://next.url")).to.be.true
|
||||||
|
done()
|
||||||
|
)
|
||||||
|
|
||||||
|
it "cancel back to previous url", () ->
|
||||||
|
$scope = $rootScope.$new()
|
||||||
|
|
||||||
|
mocks.routeParams.application = 6
|
||||||
|
mocks.routeParams.state = "testing-state"
|
||||||
|
|
||||||
|
applicationToken = Immutable.fromJS({})
|
||||||
|
mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken)
|
||||||
|
|
||||||
|
ctrl = $controller("ExternalApp")
|
||||||
|
expect(mocks.window.history.back.callCount).to.be.equal(0)
|
||||||
|
ctrl.cancel()
|
||||||
|
expect(mocks.window.history.back.callCount).to.be.equal(1)
|
|
@ -0,0 +1,28 @@
|
||||||
|
section.external-app-wrapper
|
||||||
|
div.logo
|
||||||
|
include ../../svg/logo-color.svg
|
||||||
|
|
||||||
|
h1 Taiga
|
||||||
|
|
||||||
|
h2(translate="EXTERNAL_APP.AUTHORIZATION_REQUEST", translate-values="{application: vm.application.get('name')}")
|
||||||
|
|
||||||
|
div.user-card.avatar
|
||||||
|
.card-inner
|
||||||
|
div.user-image
|
||||||
|
img(ng-src="{{::vm.user.get('photo')}}", alt="{{::vm.user.get('full_name_display')}}")
|
||||||
|
div.user-data
|
||||||
|
h3 {{ ::vm.user.get("full_name_display") }}
|
||||||
|
p {{ ::vm.user.get("email") }}
|
||||||
|
a(ng-href="{{::vm.loginWithAnotherUserUrl}}", title="{{'EXTERNAL_APP.LOGIN_WITH_ANOTHER_USER' | translate}}", translate="EXTERNAL_APP.LOGIN_WITH_ANOTHER_USER")
|
||||||
|
|
||||||
|
div.app-card
|
||||||
|
.card-inner
|
||||||
|
div.app-image
|
||||||
|
img(ng-src="{{::vm.application.get('icon_url')}}", alt="{{::vm.application.get('name')}}")
|
||||||
|
div.app-data
|
||||||
|
h3 {{ ::vm.application.get("name") }}
|
||||||
|
a(ng-href="{{::vm.application.get('web')}}", title="{{::vm.application.get('name')}}", target="_blank") {{ ::vm.application.get('web') }}
|
||||||
|
p {{ ::vm.application.get("description") }}
|
||||||
|
|
||||||
|
a.button-green(href="#", ng-click="vm.createApplicationToken()", title="{{'EXTERNAL_APP.AUTHORIZE_APP' | translate}}", translate="EXTERNAL_APP.AUTHORIZE_APP")
|
||||||
|
a.cancel(href="#", ng-click="vm.cancel()", title="{{'EXTERNAL_APP.CANCEL' | translate}}", translate="EXTERNAL_APP.CANCEL")
|
|
@ -0,0 +1,85 @@
|
||||||
|
.external-app-wrapper {
|
||||||
|
margin: 2rem auto;
|
||||||
|
text-align: center;
|
||||||
|
width: 480px;
|
||||||
|
.logo {
|
||||||
|
height: 6rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.app-card,
|
||||||
|
.user-card {
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
.card-inner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
@extend %large;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
@extend %xsmall;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-card {
|
||||||
|
.app-image {
|
||||||
|
flex-basis: 100px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
max-width: 105px;
|
||||||
|
}
|
||||||
|
.app-data {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
@extend %xsmall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.user-card {
|
||||||
|
background: $card;
|
||||||
|
border: 1px solid $card-hover;
|
||||||
|
padding: 1rem;
|
||||||
|
.card-inner {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.user-image {
|
||||||
|
flex-basis: 50px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
max-width: 55px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button-green {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.cancel {
|
||||||
|
@extend %small;
|
||||||
|
display: block;
|
||||||
|
margin-top: .5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint(mobile) {
|
||||||
|
.external-app-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
class ExternalAppsService extends taiga.Service
|
||||||
|
@.$inject = [
|
||||||
|
"tgResources"
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@rs) ->
|
||||||
|
|
||||||
|
getApplicationToken: (applicationId, state) ->
|
||||||
|
return @rs.externalapps.getApplicationToken(applicationId, state)
|
||||||
|
|
||||||
|
authorizeApplicationToken: (applicationId, state) ->
|
||||||
|
return @rs.externalapps.authorizeApplicationToken(applicationId, state)
|
||||||
|
|
||||||
|
angular.module("taigaExternalApps").service("tgExternalAppsService", ExternalAppsService)
|
|
@ -0,0 +1,44 @@
|
||||||
|
describe "tgExternalAppsService", ->
|
||||||
|
externalAppsService = provide = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
_mockTgResources = () ->
|
||||||
|
mocks.tgResources = {
|
||||||
|
externalapps: {
|
||||||
|
getApplicationToken: sinon.stub()
|
||||||
|
authorizeApplicationToken: sinon.stub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "tgResources", mocks.tgResources
|
||||||
|
|
||||||
|
_inject = (callback) ->
|
||||||
|
inject (_tgExternalAppsService_) ->
|
||||||
|
externalAppsService = _tgExternalAppsService_
|
||||||
|
callback() if callback
|
||||||
|
|
||||||
|
_mocks = () ->
|
||||||
|
module ($provide) ->
|
||||||
|
provide = $provide
|
||||||
|
_mockTgResources()
|
||||||
|
return null
|
||||||
|
|
||||||
|
_setup = ->
|
||||||
|
_mocks()
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaExternalApps"
|
||||||
|
_setup()
|
||||||
|
_inject()
|
||||||
|
|
||||||
|
it "getApplicationToken", () ->
|
||||||
|
expect(mocks.tgResources.externalapps.getApplicationToken.callCount).to.be.equal(0)
|
||||||
|
externalAppsService.getApplicationToken(6, "testing-state")
|
||||||
|
expect(mocks.tgResources.externalapps.getApplicationToken.callCount).to.be.equal(1)
|
||||||
|
expect(mocks.tgResources.externalapps.getApplicationToken.calledWith(6, "testing-state")).to.be.true
|
||||||
|
|
||||||
|
it "authorizeApplicationToken", () ->
|
||||||
|
expect(mocks.tgResources.externalapps.authorizeApplicationToken.callCount).to.be.equal(0)
|
||||||
|
externalAppsService.authorizeApplicationToken(6, "testing-state")
|
||||||
|
expect(mocks.tgResources.externalapps.authorizeApplicationToken.callCount).to.be.equal(1)
|
||||||
|
expect(mocks.tgResources.externalapps.authorizeApplicationToken.calledWith(6, "testing-state")).to.be.true
|
|
@ -0,0 +1 @@
|
||||||
|
module = angular.module("taigaExternalApps", [])
|
|
@ -1,4 +1,4 @@
|
||||||
NavigationBarDirective = (currentUserService, $location) ->
|
NavigationBarDirective = (currentUserService, navigationBarService, $location) ->
|
||||||
link = (scope, el, attrs, ctrl) ->
|
link = (scope, el, attrs, ctrl) ->
|
||||||
scope.vm = {}
|
scope.vm = {}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ NavigationBarDirective = (currentUserService, $location) ->
|
||||||
|
|
||||||
taiga.defineImmutableProperty(scope.vm, "projects", () -> currentUserService.projects.get("recents"))
|
taiga.defineImmutableProperty(scope.vm, "projects", () -> currentUserService.projects.get("recents"))
|
||||||
taiga.defineImmutableProperty(scope.vm, "isAuthenticated", () -> currentUserService.isAuthenticated())
|
taiga.defineImmutableProperty(scope.vm, "isAuthenticated", () -> currentUserService.isAuthenticated())
|
||||||
|
taiga.defineImmutableProperty(scope.vm, "isEnabledHeader", () -> navigationBarService.isEnabledHeader())
|
||||||
|
|
||||||
|
|
||||||
directive = {
|
directive = {
|
||||||
templateUrl: "navigation-bar/navigation-bar.html"
|
templateUrl: "navigation-bar/navigation-bar.html"
|
||||||
|
@ -21,6 +23,7 @@ NavigationBarDirective = (currentUserService, $location) ->
|
||||||
|
|
||||||
NavigationBarDirective.$inject = [
|
NavigationBarDirective.$inject = [
|
||||||
"tgCurrentUserService",
|
"tgCurrentUserService",
|
||||||
|
"tgNavigationBarService"
|
||||||
"$location"
|
"$location"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
nav.navbar
|
nav.navbar(ng-if="vm.isEnabledHeader")
|
||||||
div.nav-left
|
div.nav-left
|
||||||
a.logo(
|
a.logo(
|
||||||
href="#",
|
href="#",
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
class NavigationBarService extends taiga.Service
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@.disableHeader()
|
||||||
|
|
||||||
|
enableHeader: ->
|
||||||
|
@.enabledHeader = true
|
||||||
|
|
||||||
|
disableHeader: ->
|
||||||
|
@.enabledHeader = false
|
||||||
|
|
||||||
|
isEnabledHeader: ->
|
||||||
|
return @.enabledHeader
|
||||||
|
|
||||||
|
angular.module("taigaNavigationBar").service("tgNavigationBarService", NavigationBarService)
|
|
@ -0,0 +1,27 @@
|
||||||
|
Resource = (urlsService, http) ->
|
||||||
|
service = {}
|
||||||
|
|
||||||
|
service.getApplicationToken = (applicationId, state) ->
|
||||||
|
url = urlsService.resolve("applications")
|
||||||
|
url = "#{url}/#{applicationId}/token?state=#{state}"
|
||||||
|
return http.get(url).then (result) ->
|
||||||
|
Immutable.fromJS(result.data)
|
||||||
|
|
||||||
|
service.authorizeApplicationToken = (applicationId, state) ->
|
||||||
|
url = urlsService.resolve("application-tokens")
|
||||||
|
url = "#{url}/authorize"
|
||||||
|
data = {
|
||||||
|
"state": state
|
||||||
|
"application": applicationId
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.post(url, data).then (result) ->
|
||||||
|
Immutable.fromJS(result.data)
|
||||||
|
|
||||||
|
return () ->
|
||||||
|
return {"externalapps": service}
|
||||||
|
|
||||||
|
Resource.$inject = ["$tgUrls", "$tgHttp"]
|
||||||
|
|
||||||
|
module = angular.module("taigaResources2")
|
||||||
|
module.factory("tgExternalAppsResource", Resource)
|
|
@ -3,7 +3,8 @@ services = [
|
||||||
"tgUsersResources",
|
"tgUsersResources",
|
||||||
"tgUserstoriesResource",
|
"tgUserstoriesResource",
|
||||||
"tgTasksResource",
|
"tgTasksResource",
|
||||||
"tgIssuesResource"
|
"tgIssuesResource",
|
||||||
|
"tgExternalAppsResource"
|
||||||
]
|
]
|
||||||
|
|
||||||
Resources = ($injector) ->
|
Resources = ($injector) ->
|
||||||
|
|
|
@ -59,5 +59,13 @@ class AppMetaService extends taiga.Service = ->
|
||||||
@.setTwitterMetas(title, description)
|
@.setTwitterMetas(title, description)
|
||||||
@.setOpenGraphMetas(title, description)
|
@.setOpenGraphMetas(title, description)
|
||||||
|
|
||||||
|
addMobileViewport: () ->
|
||||||
|
$('head').append(
|
||||||
|
'<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">'
|
||||||
|
)
|
||||||
|
|
||||||
|
removeMobileViewport: () ->
|
||||||
|
$('meta[name="viewport"]').remove()
|
||||||
|
|
||||||
|
|
||||||
angular.module("taigaCommon").service("tgAppMetaService", AppMetaService)
|
angular.module("taigaCommon").service("tgAppMetaService", AppMetaService)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
include ../includes/components/beta
|
div
|
||||||
|
include ../includes/components/beta
|
||||||
|
|
||||||
div.wrapper
|
div.wrapper
|
||||||
div.login-main
|
div.login-main
|
||||||
div.login-container
|
div.login-container
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.beta {
|
.beta {
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -40px;
|
top: 0;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,7 @@
|
||||||
@else if $point == tablet {
|
@else if $point == tablet {
|
||||||
@media (max-width: 767px) { @content ; }
|
@media (max-width: 767px) { @content ; }
|
||||||
}
|
}
|
||||||
@else if $point == mobileonly {
|
@else if $point == mobile {
|
||||||
@media (max-width: 480px) { @content ; }
|
@media (max-width: 480px) { @content ; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg preserveAspectRatio="xMidYMid meet" version="1.1" viewBox="0 0 267.204 267.245" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M228.256 39.167l-11.755 82.74-82.74 11.756 11.756-82.74z" fill="#a295ae" opacity=".8"/>
|
||||||
|
<path d="M39.19 228.134l11.755-82.74 82.74-11.756-11.755 82.74z" fill="#5d6f6d" opacity=".8"/>
|
||||||
|
<path d="M39.19 39.277l82.74 11.755 11.756 82.74-82.74-11.755z" fill="#8cd592" opacity=".8"/>
|
||||||
|
<path d="M228.16 228.144l-82.74-11.755-11.756-82.742 82.74 11.756z" fill="#665e74" opacity=".8"/>
|
||||||
|
<path d="M133.738 267.245l-50.194-66.82 50.194-66.817 50.194 66.818z" fill="#3c3647" opacity=".8"/>
|
||||||
|
<path d="M267.204 133.48l-66.82 50.195-66.817-50.194 66.818-50.193z" fill="#837193" opacity=".8"/>
|
||||||
|
<path d="M133.616 0l50.194 66.82-50.194 66.817L83.422 66.82z" fill="#a2f4ac" opacity=".8"/>
|
||||||
|
<path d="M0 133.706l66.82-50.194 66.817 50.194L66.82 183.9z" fill="#7ea685" opacity=".8"/>
|
||||||
|
<path d="M133.602 101.85l31.772 31.772-31.772 31.772-31.772-31.772z" fill="#3c3647"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -1,6 +1,6 @@
|
||||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 134.2 134.3" version="1.1" preserveAspectRatio="xMidYMid meet">
|
<svg id="logo-svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 134.2 134.3" version="1.1" preserveAspectRatio="xMidYMid meet">
|
||||||
<style>
|
<style>
|
||||||
path {
|
#logo-svg path {
|
||||||
fill:#f5f5f5;
|
fill:#f5f5f5;
|
||||||
opacity:0.7;
|
opacity:0.7;
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Loading…
Reference in New Issue