diff --git a/CHANGELOG.md b/CHANGELOG.md
index d310bc17..c9abc248 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee
index bc5b7ca4..acfe69ab 100644
--- a/app/coffee/app.coffee
+++ b/app/coffee/app.coffee
@@ -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
])
diff --git a/app/coffee/modules/auth.coffee b/app/coffee/modules/auth.coffee
index ddab6374..88a601a0 100644
--- a/app/coffee/modules/auth.coffee
+++ b/app/coffee/modules/auth.coffee
@@ -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"))
diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee
index 0bfd7199..f17841e1 100644
--- a/app/coffee/modules/resources.coffee
+++ b/app/coffee/modules/resources.coffee
@@ -149,6 +149,10 @@ urls = {
# locales
"locales": "/locales"
+
+ # Application tokens
+ "applications": "/applications"
+ "application-tokens": "/application-tokens"
}
# Initialize api urls service
diff --git a/app/locales/locale-en.json b/app/locales/locale-en.json
index f465eb4c..42ab4fbc 100644
--- a/app/locales/locale-en.json
+++ b/app/locales/locale-en.json
@@ -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"
}
}
diff --git a/app/modules/external-apps/external-app.controller.coffee b/app/modules/external-apps/external-app.controller.coffee
new file mode 100644
index 00000000..faec88ca
--- /dev/null
+++ b/app/modules/external-apps/external-app.controller.coffee
@@ -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)
diff --git a/app/modules/external-apps/external-app.controller.spec.coffee b/app/modules/external-apps/external-app.controller.spec.coffee
new file mode 100644
index 00000000..d2ea1a9b
--- /dev/null
+++ b/app/modules/external-apps/external-app.controller.spec.coffee
@@ -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)
diff --git a/app/modules/external-apps/external-app.jade b/app/modules/external-apps/external-app.jade
new file mode 100644
index 00000000..59d42918
--- /dev/null
+++ b/app/modules/external-apps/external-app.jade
@@ -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")
diff --git a/app/modules/external-apps/external-app.scss b/app/modules/external-apps/external-app.scss
new file mode 100644
index 00000000..9a387368
--- /dev/null
+++ b/app/modules/external-apps/external-app.scss
@@ -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%;
+ }
+}
diff --git a/app/modules/external-apps/external-app.service.coffee b/app/modules/external-apps/external-app.service.coffee
new file mode 100644
index 00000000..2a301d13
--- /dev/null
+++ b/app/modules/external-apps/external-app.service.coffee
@@ -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)
diff --git a/app/modules/external-apps/external-app.service.spec.coffee b/app/modules/external-apps/external-app.service.spec.coffee
new file mode 100644
index 00000000..92af8108
--- /dev/null
+++ b/app/modules/external-apps/external-app.service.spec.coffee
@@ -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
diff --git a/app/modules/external-apps/external-apps.module.coffee b/app/modules/external-apps/external-apps.module.coffee
new file mode 100644
index 00000000..4db50ec7
--- /dev/null
+++ b/app/modules/external-apps/external-apps.module.coffee
@@ -0,0 +1 @@
+module = angular.module("taigaExternalApps", [])
diff --git a/app/modules/navigation-bar/navigation-bar.directive.coffee b/app/modules/navigation-bar/navigation-bar.directive.coffee
index e4da024d..b941b398 100644
--- a/app/modules/navigation-bar/navigation-bar.directive.coffee
+++ b/app/modules/navigation-bar/navigation-bar.directive.coffee
@@ -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"
]
diff --git a/app/modules/navigation-bar/navigation-bar.jade b/app/modules/navigation-bar/navigation-bar.jade
index e1b842c9..f8202abf 100644
--- a/app/modules/navigation-bar/navigation-bar.jade
+++ b/app/modules/navigation-bar/navigation-bar.jade
@@ -1,4 +1,4 @@
-nav.navbar
+nav.navbar(ng-if="vm.isEnabledHeader")
div.nav-left
a.logo(
href="#",
diff --git a/app/modules/navigation-bar/navigation-bar.service.coffee b/app/modules/navigation-bar/navigation-bar.service.coffee
new file mode 100644
index 00000000..48e52e1b
--- /dev/null
+++ b/app/modules/navigation-bar/navigation-bar.service.coffee
@@ -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)
diff --git a/app/modules/resources/external-apps-resource.service.coffee b/app/modules/resources/external-apps-resource.service.coffee
new file mode 100644
index 00000000..f43d14aa
--- /dev/null
+++ b/app/modules/resources/external-apps-resource.service.coffee
@@ -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)
diff --git a/app/modules/resources/resources.coffee b/app/modules/resources/resources.coffee
index b3fecee0..8d73ce4d 100644
--- a/app/modules/resources/resources.coffee
+++ b/app/modules/resources/resources.coffee
@@ -3,7 +3,8 @@ services = [
"tgUsersResources",
"tgUserstoriesResource",
"tgTasksResource",
- "tgIssuesResource"
+ "tgIssuesResource",
+ "tgExternalAppsResource"
]
Resources = ($injector) ->
diff --git a/app/modules/services/app-meta.service.coffee b/app/modules/services/app-meta.service.coffee
index ca31dcb5..eee818b0 100644
--- a/app/modules/services/app-meta.service.coffee
+++ b/app/modules/services/app-meta.service.coffee
@@ -59,5 +59,13 @@ class AppMetaService extends taiga.Service = ->
@.setTwitterMetas(title, description)
@.setOpenGraphMetas(title, description)
+ addMobileViewport: () ->
+ $('head').append(
+ ''
+ )
+
+ removeMobileViewport: () ->
+ $('meta[name="viewport"]').remove()
+
angular.module("taigaCommon").service("tgAppMetaService", AppMetaService)
diff --git a/app/partials/auth/login.jade b/app/partials/auth/login.jade
index fadbd2f2..4cb2950d 100644
--- a/app/partials/auth/login.jade
+++ b/app/partials/auth/login.jade
@@ -1,6 +1,8 @@
doctype html
-include ../includes/components/beta
+div
+ include ../includes/components/beta
+
div.wrapper
div.login-main
div.login-container
diff --git a/app/styles/components/beta.scss b/app/styles/components/beta.scss
index 4e720fdf..6856d586 100644
--- a/app/styles/components/beta.scss
+++ b/app/styles/components/beta.scss
@@ -1,6 +1,6 @@
.beta {
left: 0;
position: absolute;
- top: -40px;
+ top: 0;
z-index: 9999;
}
diff --git a/app/styles/dependencies/responsive.scss b/app/styles/dependencies/responsive.scss
index 94218c7f..e426b4e6 100644
--- a/app/styles/dependencies/responsive.scss
+++ b/app/styles/dependencies/responsive.scss
@@ -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 ; }
-
}
}
diff --git a/app/svg/logo-color.svg b/app/svg/logo-color.svg
new file mode 100644
index 00000000..3ba218cd
--- /dev/null
+++ b/app/svg/logo-color.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/app/svg/logo.svg b/app/svg/logo.svg
index 346d8115..ef01257e 100644
--- a/app/svg/logo.svg
+++ b/app/svg/logo.svg
@@ -1,6 +1,6 @@
-