External apps authentication
parent
6b837d2067
commit
f52a4ba788
|
@ -10,6 +10,7 @@
|
|||
- Ability to choose a theme (thanks to [@astagi](https://github.com/astagi))
|
||||
- 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)).
|
||||
- Support authentication via Application Tokens
|
||||
- i18n.
|
||||
- Add polish (pl) translation.
|
||||
- Add portuguese (Brazil) (pt_BR) translation.
|
||||
|
|
|
@ -68,7 +68,6 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
},
|
||||
title: "HOME.PAGE_TITLE",
|
||||
description: "HOME.PAGE_DESCRIPTION",
|
||||
loader: true
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -334,22 +333,25 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
$routeProvider.when("/login",
|
||||
{
|
||||
templateUrl: "auth/login.html",
|
||||
title: "LOGIN.PAGE_TITLE"
|
||||
description: "LOGIN.PAGE_DESCRIPTION"
|
||||
title: "LOGIN.PAGE_TITLE",
|
||||
description: "LOGIN.PAGE_DESCRIPTION",
|
||||
disableHeader: true
|
||||
}
|
||||
)
|
||||
$routeProvider.when("/register",
|
||||
{
|
||||
templateUrl: "auth/register.html",
|
||||
title: "REGISTER.PAGE_TITLE",
|
||||
description: "REGISTER.PAGE_DESCRIPTION"
|
||||
description: "REGISTER.PAGE_DESCRIPTION",
|
||||
disableHeader: true
|
||||
}
|
||||
)
|
||||
$routeProvider.when("/forgot-password",
|
||||
{
|
||||
templateUrl: "auth/forgot-password.html",
|
||||
title: "FORGOT_PASSWORD.PAGE_TITLE",
|
||||
description: "FORGOT_PASSWORD.PAGE_DESCRIPTION"
|
||||
description: "FORGOT_PASSWORD.PAGE_DESCRIPTION",
|
||||
disableHeader: true
|
||||
}
|
||||
)
|
||||
$routeProvider.when("/change-password",
|
||||
|
@ -357,6 +359,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
templateUrl: "auth/change-password-from-recovery.html",
|
||||
title: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||
description: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||
disableHeader: true
|
||||
}
|
||||
)
|
||||
$routeProvider.when("/change-password/:token",
|
||||
|
@ -364,13 +367,29 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
templateUrl: "auth/change-password-from-recovery.html",
|
||||
title: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||
description: "CHANGE_PASSWORD.PAGE_TITLE",
|
||||
disableHeader: true
|
||||
}
|
||||
)
|
||||
$routeProvider.when("/invitation/:token",
|
||||
{
|
||||
templateUrl: "auth/invitation.html",
|
||||
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.replace()
|
||||
else if response.status == 401
|
||||
nextPath = $location.path()
|
||||
$location.url($navUrls.resolve("login")).search("next=#{nextPath}")
|
||||
nextUrl = encodeURIComponent($location.url())
|
||||
$location.url($navUrls.resolve("login")).search("next=#{nextUrl}")
|
||||
|
||||
return $q.reject(response)
|
||||
|
||||
|
@ -537,7 +556,7 @@ i18nInit = (lang, $translate) ->
|
|||
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")
|
||||
|
||||
# Taiga Plugins
|
||||
|
@ -590,6 +609,15 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na
|
|||
description = $translate.instant(next.description or "")
|
||||
appMetaService.setAll(title, description)
|
||||
|
||||
if next.mobileViewport
|
||||
appMetaService.addMobileViewport()
|
||||
else
|
||||
appMetaService.removeMobileViewport()
|
||||
|
||||
if next.disableHeader
|
||||
navigationBarService.disableHeader()
|
||||
else
|
||||
navigationBarService.enableHeader()
|
||||
|
||||
modules = [
|
||||
# Main Global Modules
|
||||
|
@ -625,6 +653,7 @@ modules = [
|
|||
"taigaProfile",
|
||||
"taigaHome",
|
||||
"taigaUserTimeline",
|
||||
"taigaExternalApps",
|
||||
|
||||
# template cache
|
||||
"templates",
|
||||
|
@ -664,5 +693,6 @@ module.run([
|
|||
"tgAppMetaService",
|
||||
"tgProjectService",
|
||||
"tgLoader",
|
||||
"tgNavigationBarService"
|
||||
init
|
||||
])
|
||||
|
|
|
@ -221,12 +221,12 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $
|
|||
link = ($scope, $el, $attrs) ->
|
||||
onSuccess = (response) ->
|
||||
if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login")
|
||||
nextUrl = $routeParams['next']
|
||||
nextUrl = decodeURIComponent($routeParams['next'])
|
||||
else
|
||||
nextUrl = $navUrls.resolve("home")
|
||||
|
||||
$events.setupConnection()
|
||||
$location.path(nextUrl)
|
||||
$location.url(nextUrl)
|
||||
|
||||
onError = (response) ->
|
||||
$confirm.notify("light-error", $translate.instant("LOGIN_FORM.ERROR_AUTH_INCORRECT"))
|
||||
|
|
|
@ -149,6 +149,10 @@ urls = {
|
|||
|
||||
# locales
|
||||
"locales": "/locales"
|
||||
|
||||
# Application tokens
|
||||
"applications": "/applications"
|
||||
"application-tokens": "/application-tokens"
|
||||
}
|
||||
|
||||
# Initialize api urls service
|
||||
|
|
|
@ -1268,5 +1268,13 @@
|
|||
"BLOCKED": "{{username}} has blocked {{obj_name}}",
|
||||
"UNBLOCKED": "{{username}} has unblocked {{obj_name}}",
|
||||
"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) ->
|
||||
scope.vm = {}
|
||||
|
||||
|
@ -10,6 +10,8 @@ NavigationBarDirective = (currentUserService, $location) ->
|
|||
|
||||
taiga.defineImmutableProperty(scope.vm, "projects", () -> currentUserService.projects.get("recents"))
|
||||
taiga.defineImmutableProperty(scope.vm, "isAuthenticated", () -> currentUserService.isAuthenticated())
|
||||
taiga.defineImmutableProperty(scope.vm, "isEnabledHeader", () -> navigationBarService.isEnabledHeader())
|
||||
|
||||
|
||||
directive = {
|
||||
templateUrl: "navigation-bar/navigation-bar.html"
|
||||
|
@ -21,6 +23,7 @@ NavigationBarDirective = (currentUserService, $location) ->
|
|||
|
||||
NavigationBarDirective.$inject = [
|
||||
"tgCurrentUserService",
|
||||
"tgNavigationBarService"
|
||||
"$location"
|
||||
]
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
nav.navbar
|
||||
nav.navbar(ng-if="vm.isEnabledHeader")
|
||||
div.nav-left
|
||||
a.logo(
|
||||
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",
|
||||
"tgUserstoriesResource",
|
||||
"tgTasksResource",
|
||||
"tgIssuesResource"
|
||||
"tgIssuesResource",
|
||||
"tgExternalAppsResource"
|
||||
]
|
||||
|
||||
Resources = ($injector) ->
|
||||
|
|
|
@ -59,5 +59,13 @@ class AppMetaService extends taiga.Service = ->
|
|||
@.setTwitterMetas(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)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
doctype html
|
||||
|
||||
div
|
||||
include ../includes/components/beta
|
||||
|
||||
div.wrapper
|
||||
div.login-main
|
||||
div.login-container
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.beta {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,7 @@
|
|||
@else if $point == tablet {
|
||||
@media (max-width: 767px) { @content ; }
|
||||
}
|
||||
@else if $point == mobileonly {
|
||||
@else if $point == mobile {
|
||||
@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>
|
||||
path {
|
||||
#logo-svg path {
|
||||
fill:#f5f5f5;
|
||||
opacity:0.7;
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Loading…
Reference in New Issue