diff options
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | README.markdown | 3 | ||||
-rw-r--r-- | app/Controller/Base.php | 1 | ||||
-rw-r--r-- | app/Controller/User.php | 64 | ||||
-rw-r--r-- | app/Locales/de_DE/translations.php | 108 | ||||
-rw-r--r-- | app/Locales/es_ES/translations.php | 8 | ||||
-rw-r--r-- | app/Locales/fr_FR/translations.php | 8 | ||||
-rw-r--r-- | app/Locales/pl_PL/translations.php | 8 | ||||
-rw-r--r-- | app/Locales/pt_BR/translations.php | 8 | ||||
-rw-r--r-- | app/Locales/sv_SE/translations.php | 8 | ||||
-rw-r--r-- | app/Locales/zh_CN/translations.php | 8 | ||||
-rw-r--r-- | app/Model/Acl.php | 4 | ||||
-rw-r--r-- | app/Model/GitHub.php | 178 | ||||
-rw-r--r-- | app/Model/LastLogin.php | 1 | ||||
-rw-r--r-- | app/Model/User.php | 12 | ||||
-rw-r--r-- | app/Schema/Mysql.php | 7 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 7 | ||||
-rw-r--r-- | app/Templates/user_edit.php | 13 | ||||
-rw-r--r-- | app/Templates/user_login.php | 12 | ||||
-rw-r--r-- | app/common.php | 5 | ||||
-rw-r--r-- | assets/css/app.css | 10 | ||||
-rw-r--r-- | config.default.php | 9 | ||||
-rw-r--r-- | docs/github-authentication.markdown | 62 |
23 files changed, 488 insertions, 57 deletions
diff --git a/.travis.yml b/.travis.yml index cc70a9d7..edd10551 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: php php: + - "5.6" - "5.5" - "5.4" - "5.3" diff --git a/README.markdown b/README.markdown index 3355fa16..2f8138b7 100644 --- a/README.markdown +++ b/README.markdown @@ -24,7 +24,7 @@ Features - Tasks with different colors, categories, sub-tasks, attachments, Markdown support for the description - Automatic actions - Users management with a basic privileges separation (administrator or regular user) -- External authentication: Google Account and LDAP/ActiveDirectory +- External authentication: Google and GitHub accounts as well as LDAP/ActiveDirectory - Webhooks to create tasks from an external software - Host anywhere (shared hosting, VPS, Raspberry Pi or localhost) - No external dependencies @@ -104,6 +104,7 @@ Documentation - [LDAP authentication](docs/ldap-authentication.markdown) - [Google authentication](docs/google-authentication.markdown) +- [GitHub authentication](docs/github-authentication.markdown) #### Developers diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 13fb9b91..25a72f15 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -20,6 +20,7 @@ use Model\LastLogin; * @property \Model\Config $config * @property \Model\File $file * @property \Model\Google $google + * @property \Model\GitHub $gitHub * @property \Model\LastLogin $lastLogin * @property \Model\Ldap $ldap * @property \Model\Project $project diff --git a/app/Controller/User.php b/app/Controller/User.php index fca33b28..d30c6fd2 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -299,4 +299,68 @@ class User extends Base $this->response->redirect('?controller=user'); } + + /** + * GitHub authentication + * + * @access public + */ + public function gitHub() + { + $code = $this->request->getStringParam('code'); + + if ($code) { + $profile = $this->gitHub->getGitHubProfile($code); + + if (is_array($profile)) { + + // If the user is already logged, link the account otherwise authenticate + if ($this->acl->isLogged()) { + + if ($this->gitHub->updateUser($this->acl->getUserId(), $profile)) { + $this->session->flash(t('Your GitHub account was successfully linked to your profile.')); + } + else { + $this->session->flashError(t('Unable to link your GitHub Account.')); + } + + $this->response->redirect('?controller=user'); + } + else if ($this->gitHub->authenticate($profile['id'])) { + $this->response->redirect('?controller=app'); + } + else { + $this->response->html($this->template->layout('user_login', array( + 'errors' => array('login' => t('GitHub authentication failed')), + 'values' => array(), + 'no_layout' => true, + 'title' => t('Login') + ))); + } + } + } + + $this->response->redirect($this->gitHub->getAuthorizationUrl()); + } + + /** + * Unlink a GitHub account + * + * @access public + */ + public function unlinkGitHub() + { + $this->checkCSRFParam(); + + $this->gitHub->revokeGitHubAccess(); + + if ($this->gitHub->unlink($this->acl->getUserId())) { + $this->session->flash(t('Your GitHub account is no longer linked to your profile.')); + } + else { + $this->session->flashError(t('Unable to unlink your GitHub Account.')); + } + + $this->response->redirect('?controller=user'); + } } diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php index 48c7ee16..734c3437 100644 --- a/app/Locales/de_DE/translations.php +++ b/app/Locales/de_DE/translations.php @@ -5,9 +5,9 @@ return array( 'German' => 'Deutsch', 'French' => 'Französisch', 'Polish' => 'Polnisch', - 'Portuguese (Brazilian)' => 'Portugisisch (Brasilien)', + 'Portuguese (Brazilian)' => 'Portugiesisch (Brasilien)', 'Spanish' => 'Spanisch', - 'Chinese (Simplified)' => 'Chinesisch', + 'Chinese (Simplified)' => 'Chinesisch (vereinfacht)', 'None' => 'Kein', 'edit' => 'bearbeiten', 'Edit' => 'Bearbeiten', @@ -27,16 +27,16 @@ return array( 'Grey' => 'Grau', 'Save' => 'Speichern', 'Login' => 'Anmelden', - 'Official website:' => 'Offizielle Website :', + 'Official website:' => 'Offizielle Website:', 'Unassigned' => 'Nicht zugeordnet', 'View this task' => 'Aufgabe ansehen', 'Remove user' => 'Benutzer löschen', - 'Do you really want to remove this user: "%s"?' => 'Willst Du diesen Benutzer wirklich löschen : « %s » ?', + 'Do you really want to remove this user: "%s"?' => 'Willst Du diesen Benutzer wirklich löschen: «%s»?', 'New user' => 'Neuer Benutzer', 'All users' => 'Alle Benutzer', 'Username' => 'Benutzername', 'Password' => 'Passwort', - 'Default Project' => 'Standard Projekt', + 'Default Project' => 'Standardprojekt', 'Administrator' => 'Administrator', 'Sign in' => 'Anmelden', 'Users' => 'Benutzer', @@ -70,37 +70,37 @@ return array( 'Disable' => 'Deaktivieren', 'Enable' => 'Aktivieren', 'New project' => 'Neues Projekt', - 'Do you really want to remove this project: "%s"?' => 'Möchtest Du dieses Projekt wirklich löschen : « %s » ?', + 'Do you really want to remove this project: "%s"?' => 'Möchtest Du dieses Projekt wirklich löschen: «%s»?', 'Remove project' => 'Projekt löschen', 'Boards' => 'Tafel', - 'Edit the board for "%s"' => 'Modifier le tableau pour « %s »', + 'Edit the board for "%s"' => 'Tafel für «%s» bearbeiten', 'All projects' => 'Alle Projekte', 'Change columns' => 'Spalten ändern', 'Add a new column' => 'Neue Spalte hinzufügen', 'Title' => 'Titel', 'Add Column' => 'Neue Spalte', - 'Project "%s"' => 'Projekt « %s »', + 'Project "%s"' => 'Projekt «%s»', 'Nobody assigned' => 'Kein Zuständiger zugeordnet', 'Assigned to %s' => 'Zuständiger: %s', 'Remove a column' => 'Spalte löschen', 'Remove a column from a board' => 'Eine Spalte von einer Tafel löschen', 'Unable to remove this column.' => 'Löschen dieser Spalte nicht möglich.', - 'Do you really want to remove this column: "%s"?' => 'Willst Du diese Spalte wirklich löschen : « %s » ?', - 'This action will REMOVE ALL TASKS associated to this column!' => 'Diese Aktion wird ALLE AUFGABEN löschen !', + 'Do you really want to remove this column: "%s"?' => 'Willst Du diese Spalte wirklich löschen: «%s»?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Diese Aktion wird ALLE AUFGABEN löschen!', 'Settings' => 'Einstellungen', 'Application settings' => 'Applikationseinstellungen', 'Language' => 'Sprache', 'Webhooks token:' => 'Webhooks token:', 'More information' => 'Mehr Informationen', - 'Database size:' => 'Datenbank Größe :', + 'Database size:' => 'Datenbankgröße:', 'Download the database' => 'Download der Datenbank', 'Optimize the database' => 'Optimieren der Datenbank', '(VACUUM command)' => '(VACUUM command)', '(Gzip compressed Sqlite file)' => '(Gzip komprimierte sqlite Datei)', - 'User settings' => 'Benutzer Eintellungen', - 'My default project:' => 'Mein Standard Projekt : ', + 'User settings' => 'Benutzereinstellungen', + 'My default project:' => 'Mein Standardprojekt:', 'Close a task' => 'Aufgabe schließen', - 'Do you really want to close this task: "%s"?' => 'Willst Du diese Aufgabe wirklich schließen : « %s » ?', + 'Do you really want to close this task: "%s"?' => 'Willst Du diese Aufgabe wirklich schließen: «%s»?', 'Edit a task' => 'Aufgabe bearbeiten', 'Column' => 'Spalte', 'Color' => 'Farbe', @@ -108,18 +108,18 @@ return array( 'Create another task' => 'Neue Aufgabe erstellen', 'New task' => 'Neue Aufgabe', 'Open a task' => 'Öffne eine Aufgabe', - 'Do you really want to open this task: "%s"?' => 'Willst Du diese Aufgabe wirklich öffnen : « %s » ?', + 'Do you really want to open this task: "%s"?' => 'Willst Du diese Aufgabe wirklich öffnen: «%s»?', 'Back to the board' => 'Zurück zur Tafel', 'Created on %B %e, %G at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M', 'There is nobody assigned' => 'Ein gibt keinen Zuständigen', - 'Column on the board:' => 'Spalten auf diesem Tafel : ', + 'Column on the board:' => 'Spalten auf diesem Tafel:', 'Status is open' => 'Status ist geöffnet', 'Status is closed' => 'Status ist geschlossen', 'close this task' => 'Aufgabe schließen', 'open this task' => 'Aufgabe öffnen', - 'There is no description.' => 'Es gibt keine Erklärung.', + 'There is no description.' => 'Es gibt keine Beschreibung.', 'Add a new task' => 'Eine neue Aufgabe hinzufügen', - 'The username is required' => 'Der Butzutzername wird benötigt', + 'The username is required' => 'Der Benutzername wird benötigt', 'The maximum length is %d characters' => 'Die maximale Länge sind %d Zeichen', 'The minimum length is %d characters' => 'Die minimale Länge sind %d Zeichen', 'The password is required' => 'Das Passwort wird benötigt', @@ -133,12 +133,12 @@ return array( 'The project is required' => 'Das Projekt wird benötigt', 'The color is required' => 'Die Farbe wird benötigt', 'The id is required' => 'Die ID wird benötigt', - 'The project id is required' => 'Die Projekt ID wird benötogt', - 'The project name is required' => 'Der Projekt Name wird benötigt', - 'This project must be unique' => 'Der Projekt Name muss eindeutig sein', + 'The project id is required' => 'Die Projekt ID wird benötigt', + 'The project name is required' => 'Der Projektname wird benötigt', + 'This project must be unique' => 'Der Projektname muss eindeutig sein', 'The title is required' => 'Der Titel wird benötigt', 'The language is required' => 'Die Sprache wird benötigt', - 'There is no active project, the first step is to create a new project.' => 'Es gibt kein aktives Projekt. Der erste Schritt ist ein Projekt erstellen.', + 'There is no active project, the first step is to create a new project.' => 'Es gibt kein aktives Projekt. Der erste Schritt ist ein Projekt zu erstellen.', 'Settings saved successfully.' => 'Die Einstellungen wurden erfolgreich gespeichert.', 'Unable to save your settings.' => 'Speicher der Einstellungen nicht möglich.', 'Database optimization done.' => 'Optimieren der Datenbank abgeschlossen.', @@ -148,7 +148,7 @@ return array( 'Unable to update this project.' => 'Ändern des Projekts nicht möglich.', 'Unable to remove this project.' => 'Löschen des Projekts nicht möglich.', 'Project removed successfully.' => 'Löschen des Projekts erfolgreich.', - 'Project activated successfully.' => 'Aktivieren des Projekts erolgreich.', + 'Project activated successfully.' => 'Aktivieren des Projekts erfolgreich.', 'Unable to activate this project.' => 'Aktivieren des Projekts nicht möglich.', 'Project disabled successfully.' => 'Deaktivieren des Projekts erfolgreich.', 'Unable to disable this project.' => 'Deaktivieren des Projekts nicht möglich.', @@ -171,24 +171,24 @@ return array( 'Backlog' => 'Ideen', 'Work in progress' => 'In Arbeit', 'Done' => 'Erledigt', - 'Application version:' => 'Programmversion :', - 'Completed on %B %e, %G at %k:%M %p' => 'Erledigt am %d.%m.%Y à %H:%M', + 'Application version:' => 'Programmversion:', + 'Completed on %B %e, %G at %k:%M %p' => 'Erledigt am %d.%m.%Y um %H:%M', '%B %e, %G at %k:%M %p' => '%d.%m.%Y um %H:%M', - 'Date created' => 'Erstellung am', + 'Date created' => 'Erstellt am', 'Date completed' => 'Erledigt am', 'Id' => 'ID', 'No task' => 'Keine Aufgabe', 'Completed tasks' => 'Erledigte Aufgaben', 'List of projects' => 'Liste der Projekte', - 'Completed tasks for "%s"' => 'Erledigte Aufaben für « %s »', - '%d closed tasks' => '%d erledigte Augaben', + 'Completed tasks for "%s"' => 'Erledigte Aufgaben für «%s»', + '%d closed tasks' => '%d erledigte Aufgaben', 'no task for this project' => 'Keine Aufgaben in diesem Projekt', 'Public link' => 'Öffentlicher Link', - 'There is no column in your project!' => 'Es gibt keine Spalte in Deinem Projekt!', + 'There is no column in your project!' => 'Es gibt keine Spalte in deinem Projekt!', 'Change assignee' => 'Zuständigkeit ändern', - 'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: « %s »', + 'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: «%s»', 'Timezone' => 'Zeitzone', - 'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden !', + 'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', 'Page not found' => 'Seite nicht gefunden', 'Story Points' => 'Umfang', 'limit' => 'Limit', @@ -197,17 +197,17 @@ return array( 'Edit project access list' => 'Projekt Zugangsberechtigungen bearbeiten', 'Edit users access' => 'Benutzerzugang bearbeiten', 'Allow this user' => 'Diesen Benutzer authorisieren', - 'Project access list for "%s"' => 'Projekt Zugangsliste für « %s »', - 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt :', + 'Project access list for "%s"' => 'Projekt Zugangsliste für «%s»', + 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt:', 'Don\'t forget that administrators have access to everything.' => 'Vergiss nicht, dass Administratoren überall Zugang haben.', 'revoke' => 'verbieten', - 'List of authorized users' => 'Liste der authorisieren Benutzer', + 'List of authorized users' => 'Liste der authorisierten Benutzer', 'User' => 'Benutzer', - 'Everybody have access to this project.' => 'Jedem hat Zugang zu diesem Projekt.', + 'Everybody have access to this project.' => 'Jeder hat Zugang zu diesem Projekt.', 'You are not allowed to access to this project.' => 'Du hast keinen Zugang zu diesem Projekt.', 'Comments' => 'Kommentare', 'Post comment' => 'kommentieren', - 'Write your text in Markdown' => 'Schreibe Deinen Text in Markdown', + 'Write your text in Markdown' => 'Schreibe deinen Text in Markdown', 'Leave a comment' => 'Kommentar abgeben', 'Comment is required' => 'Ein Kommentar wird benötigt', 'Leave a description' => 'Beschreibung', @@ -226,8 +226,8 @@ return array( 'Unable to create your automatic action.' => 'Deine automatische Aktion konnte nicht erstellt werden.', 'Remove an action' => 'Aktion löschen', 'Unable to remove this action.' => 'Aktion konnte nicht gelöscht werden', - 'Action removed successfully.' => 'Action wurde erfolgreich gelöscht.', - 'Automatic actions for the project "%s"' => 'Automatische Aktionen für dieses Projekt « %s »', + 'Action removed successfully.' => 'Aktion wurde erfolgreich gelöscht.', + 'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt «%s»', 'Defined actions' => 'Definierte Aktionen', 'Add an action' => 'Aktion hinzufügen', 'Event name' => 'Ereignis', @@ -235,15 +235,15 @@ return array( 'Action parameters' => 'Aktionsparameter', 'Action' => 'Aktion', 'Event' => 'Ereignis', - 'When the selected event occurs execute the corresponding action.' => 'Wenn des gewählte Ereignis eintritt, führe die passende Aktion aus.', + 'When the selected event occurs execute the corresponding action.' => 'Wenn des gewählte Ereignis eintritt, führe die zugehörige Aktion aus.', 'Next step' => 'Nächster Schritt', 'Define action parameters' => 'Aktionsparameter definieren', 'Save this action' => 'Aktion speichern', - 'Do you really want to remove this action: "%s"?' => 'Willst Du diese Aktion wirklich löschen « %s » ?', + 'Do you really want to remove this action: "%s"?' => 'Willst Du diese Aktion wirklich löschen «%s»?', 'Remove an automatic action' => 'Löschen einer automatischen Aktion', - 'Close the task' => 'Aufgabe schießen', + 'Close the task' => 'Aufgabe schließen', 'Assign the task to a specific user' => 'Aufgabe einem Benutzer zuordnen', - 'Assign the task to the person who does the action' => 'Aufgabe dem Benutzer zuordnen der die Aktion aufgeführt hat', + 'Assign the task to the person who does the action' => 'Aufgabe dem Benutzer zuordnen der die Aktion ausgeführt hat', 'Duplicate the task to another project' => 'Aufgabe in ein anderes Projekt kopieren', 'Move a task to another column' => 'Aufgabe in eine andere Spalte verschieben', 'Move a task to another position in the same column' => 'Aufgabe an eine andere Position in der gleichen Spalte verschieben', @@ -268,7 +268,7 @@ return array( 'Do you really want to remove this comment?' => 'Willst Du diesen Kommentar wirklich löschen?', 'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars könne diese Seite verwenden.', 'Details' => 'Details', - 'Current password for the user "%s"' => 'Aktuelles Passwort für den Benutzer « %s »', + 'Current password for the user "%s"' => 'Aktuelles Passwort für den Benutzer «%s»', 'The current password is required' => 'Das aktuelle Passwort wird benötigt', 'Wrong password' => 'Falsches Passwort', 'Reset all tokens' => 'Alle Tokens zurücksetzten', @@ -278,8 +278,8 @@ return array( 'Login date' => 'Anmeldedatum', 'Authentication method' => 'Anmeldemethode', 'IP address' => 'IP Adresse', - 'User agent' => 'Benutzer Agent', - 'Persistent connections' => 'Bestende Verbindungen', + 'User agent' => 'User Agent', + 'Persistent connections' => 'Bestehende Verbindungen', 'No session' => 'Keine Session', 'Expiration date' => 'Ablaufdatum', 'Remember Me' => 'Angemeldet bleiben', @@ -291,23 +291,31 @@ return array( 'Closed' => 'Geschlossen', 'Search' => 'Suchen', 'Nothing found.' => 'Nichts gefunden.', - 'Search in the project "%s"' => 'Suche im Projekt « %s »', + 'Search in the project "%s"' => 'Suche im Projekt «%s»', 'Due date' => 'Fälligkeitsdatum', 'Others formats accepted: %s and %s' => 'Andere akzepierte Formate : %s und %s', 'Description' => 'Beschreibung', '%d comments' => '%d Kommentare', '%d comment' => '%d Kommentar', 'Email address invalid' => 'EMail Adresse ungültig', - 'Your Google Account is not linked anymore to your profile.' => 'Dein Google Accout ist nicht mit den Profil verbunden.', - 'Unable to unlink your Google Account.' => 'Trennung der Verbindung mit dem Google Accout nicht möglich.', + 'Your Google Account is not linked anymore to your profile.' => 'Dein Google Account ist nicht mehr mit deinem Profil verbunden.', + 'Unable to unlink your Google Account.' => 'Trennung der Verbindung mit deinem Google Account nicht möglich.', 'Google authentication failed' => 'Zugang mit Google nicht möglich', - 'Unable to link your Google Account.' => 'Verbindung mit dem Google Accout nicht möglich.', - 'Your Google Account is linked to your profile successfully.' => 'Der Google Account wurde erfolgreich verbunden.', + 'Unable to link your Google Account.' => 'Verbindung mit deinem Google Account nicht möglich.', + 'Your Google Account is linked to your profile successfully.' => 'Dein Google Account wurde erfolgreich verbunden.', 'Email' => 'Email', 'Link my Google Account' => 'Verbinde meinen Google Account', 'Unlink my Google Account' => 'Verbindung mit meinem Google Account trennen', 'Login with my Google Account' => 'Anmelden mit meinem Google Account', 'Project not found.' => 'Das Projekt wurde nicht gefunden.', + 'Your GitHub account was successfully linked to your profile.' => 'Dein GitHub Account wurde erfolgreich mit deinem Profil verbunden.', + 'Unable to link your GitHub Account.' => 'Verbindung mit deinem GitHub Account nicht möglich.', + 'GitHub authentication failed' => 'Zugang mit GitHub nicht möglich', + 'Your GitHub account is no longer linked to your profile.' => 'Dein GitHub Account ist nicht mehr mit deinem Profil verbunden.', + 'Unable to unlink your GitHub Account.' => 'Trennung der Verbindung mit deinem GitHub Account nicht möglich.', + 'Login with my GitHub Account' => 'Anmelden mit meinem GitHub Account', + 'Link my GitHub Account' => 'Mit meinem GitHub Account verbinden', + 'Unlink my GitHub Account' => 'Verbindung mit meinem GitHub Account trennen', // 'Task #%d' => '', // 'Task removed successfully.' => '', // 'Unable to remove this task.' => '', diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php index 70bb9ffa..98ab30ca 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locales/es_ES/translations.php @@ -376,4 +376,12 @@ return array( 'Unable to upload the file.' => 'No pude cargar el fichero.', 'Actions' => 'Acciones', // 'Display another project' => '', + // 'Your GitHub account was successfully linked to your profile.' => '', + // 'Unable to link your GitHub Account.' => '', + // 'GitHub authentication failed' => '', + // 'Your GitHub account is no longer linked to your profile.' => '', + // 'Unable to unlink your GitHub Account.' => '', + // 'Login with my GitHub Account' => '', + // 'Link my GitHub Account' => '', + // 'Unlink my GitHub Account' => '', ); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php index 37189680..4405a9a3 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locales/fr_FR/translations.php @@ -374,4 +374,12 @@ return array( 'Maximum size: ' => 'Taille maximum : ', 'Unable to upload the file.' => 'Impossible de transférer le fichier.', 'Display another project' => 'Afficher un autre projet', + 'Your GitHub account was successfully linked to your profile.' => 'Votre compte Github est désormais lié avec votre profile.', + 'Unable to link your GitHub Account.' => 'Impossible de lier votre compte Github.', + 'GitHub authentication failed' => 'L\'authentification avec Github à échouée', + 'Your GitHub account is no longer linked to your profile.' => 'Votre compte Github n\'est plus relié avec votre profile.', + 'Unable to unlink your GitHub Account.' => 'Impossible de déconnecter votre compte Github.', + 'Login with my GitHub Account' => 'Se connecter avec mon compte Github', + 'Link my GitHub Account' => 'Lier mon compte Github', + 'Unlink my GitHub Account' => 'Ne plus utiliser mon compte Github', ); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php index 1253be23..7e552d6a 100644 --- a/app/Locales/pl_PL/translations.php +++ b/app/Locales/pl_PL/translations.php @@ -377,4 +377,12 @@ return array( // 'Maximum size: ' => '', // 'Unable to upload the file.' => '', // 'Display another project' => '', + // 'Your GitHub account was successfully linked to your profile.' => '', + // 'Unable to link your GitHub Account.' => '', + // 'GitHub authentication failed' => '', + // 'Your GitHub account is no longer linked to your profile.' => '', + // 'Unable to unlink your GitHub Account.' => '', + // 'Login with my GitHub Account' => '', + // 'Link my GitHub Account' => '', + // 'Unlink my GitHub Account' => '', ); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php index 19fd412d..9a571bad 100644 --- a/app/Locales/pt_BR/translations.php +++ b/app/Locales/pt_BR/translations.php @@ -374,4 +374,12 @@ return array( // 'Maximum size: ' => '', // 'Unable to upload the file.' => '', // 'Display another project' => '', + // 'Your GitHub account was successfully linked to your profile.' => '', + // 'Unable to link your GitHub Account.' => '', + // 'GitHub authentication failed' => '', + // 'Your GitHub account is no longer linked to your profile.' => '', + // 'Unable to unlink your GitHub Account.' => '', + // 'Login with my GitHub Account' => '', + // 'Link my GitHub Account' => '', + // 'Unlink my GitHub Account' => '', ); diff --git a/app/Locales/sv_SE/translations.php b/app/Locales/sv_SE/translations.php index 79be2132..b59217a1 100644 --- a/app/Locales/sv_SE/translations.php +++ b/app/Locales/sv_SE/translations.php @@ -376,4 +376,12 @@ return array( 'Maximum size: ' => 'Maxstorlek: ', 'Unable to upload the file.' => 'Kunde inte ladda upp filen.', // 'Display another project' => '', + // 'Your GitHub account was successfully linked to your profile.' => '', + // 'Unable to link your GitHub Account.' => '', + // 'GitHub authentication failed' => '', + // 'Your GitHub account is no longer linked to your profile.' => '', + // 'Unable to unlink your GitHub Account.' => '', + // 'Login with my GitHub Account' => '', + // 'Link my GitHub Account' => '', + // 'Unlink my GitHub Account' => '', ); diff --git a/app/Locales/zh_CN/translations.php b/app/Locales/zh_CN/translations.php index 2144fcef..86628756 100644 --- a/app/Locales/zh_CN/translations.php +++ b/app/Locales/zh_CN/translations.php @@ -382,4 +382,12 @@ return array( // 'Maximum size: ' => '', // 'Unable to upload the file.' => '', // 'Display another project' => '', + // 'Your GitHub account was successfully linked to your profile.' => '', + // 'Unable to link your GitHub Account.' => '', + // 'GitHub authentication failed' => '', + // 'Your GitHub account is no longer linked to your profile.' => '', + // 'Unable to unlink your GitHub Account.' => '', + // 'Login with my GitHub Account' => '', + // 'Link my GitHub Account' => '', + // 'Unlink my GitHub Account' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index 035fd7c3..8a87a6b2 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -17,7 +17,7 @@ class Acl extends Base * @var array */ private $public_actions = array( - 'user' => array('login', 'check', 'google'), + 'user' => array('login', 'check', 'google', 'github'), 'task' => array('add'), 'board' => array('readonly'), ); @@ -32,7 +32,7 @@ class Acl extends Base 'app' => array('index'), 'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'), 'project' => array('tasks', 'index', 'forbidden', 'search'), - 'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle'), + 'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle', 'unlinkgithub'), 'config' => array('index', 'removeremembermetoken'), 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'), 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'), diff --git a/app/Model/GitHub.php b/app/Model/GitHub.php new file mode 100644 index 00000000..3380218d --- /dev/null +++ b/app/Model/GitHub.php @@ -0,0 +1,178 @@ +<?php
+
+namespace Model;
+
+require __DIR__.'/../../vendor/OAuth/bootstrap.php';
+
+use OAuth\Common\Storage\Session;
+use OAuth\Common\Consumer\Credentials;
+use OAuth\Common\Http\Uri\UriFactory;
+use OAuth\ServiceFactory;
+use OAuth\Common\Http\Exception\TokenResponseException;
+
+/**
+ * GitHub model
+ *
+ * @package model
+ */
+class GitHub extends Base
+{
+ /**
+ * Authenticate a GitHub user
+ *
+ * @access public
+ * @param string $github_id GitHub user id
+ * @return boolean
+ */
+ public function authenticate($github_id)
+ {
+ $userModel = new User($this->db, $this->event);
+
+ $user = $userModel->getByGitHubId($github_id);
+
+ if ($user) {
+
+ // Create the user session
+ $userModel->updateSession($user);
+
+ // Update login history
+ $lastLogin = new LastLogin($this->db, $this->event);
+ $lastLogin->create(
+ LastLogin::AUTH_GITHUB,
+ $user['id'],
+ $userModel->getIpAddress(),
+ $userModel->getUserAgent()
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Unlink a GitHub account for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return boolean
+ */
+ public function unlink($user_id)
+ {
+ $userModel = new User($this->db, $this->event);
+
+ return $userModel->update(array(
+ 'id' => $user_id,
+ 'github_id' => '',
+ ));
+ }
+
+ /**
+ * Update the user table based on the GitHub profile information
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param array $profile GitHub profile
+ * @return boolean
+ * @todo Don't overwrite existing email/name with empty GitHub data
+ */
+ public function updateUser($user_id, array $profile)
+ {
+ $userModel = new User($this->db, $this->event);
+
+ return $userModel->update(array(
+ 'id' => $user_id,
+ 'github_id' => $profile['id'],
+ 'email' => $profile['email'],
+ 'name' => $profile['name'],
+ ));
+ }
+
+ /**
+ * Get the GitHub service instance
+ *
+ * @access public
+ * @return \OAuth\OAuth2\Service\GitHub
+ */
+ public function getService()
+ {
+ $uriFactory = new UriFactory();
+ $currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER);
+ $currentUri->setQuery('controller=user&action=gitHub');
+
+ $storage = new Session(false);
+
+ $credentials = new Credentials(
+ GITHUB_CLIENT_ID,
+ GITHUB_CLIENT_SECRET,
+ $currentUri->getAbsoluteUri()
+ );
+
+ $serviceFactory = new ServiceFactory();
+
+ return $serviceFactory->createService(
+ 'gitHub',
+ $credentials,
+ $storage,
+ array('')
+ );
+ }
+
+ /**
+ * Get the authorization URL
+ *
+ * @access public
+ * @return \OAuth\Common\Http\Uri\Uri
+ */
+ public function getAuthorizationUrl()
+ {
+ return $this->getService()->getAuthorizationUri();
+ }
+
+ /**
+ * Get GitHub profile information from the API
+ *
+ * @access public
+ * @param string $code GitHub authorization code
+ * @return bool|array
+ */
+ public function getGitHubProfile($code)
+ {
+ try {
+ $gitHubService = $this->getService();
+ $gitHubService->requestAccessToken($code);
+
+ return json_decode($gitHubService->request('user'), true);
+ }
+ catch (TokenResponseException $e) {
+ return false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Revokes this user's GitHub tokens for Kanboard
+ *
+ * @access public
+ * @return bool|array
+ * @todo Currently this simply removes all our tokens for this user, ideally it should
+ * restrict itself to the one in question
+ */
+ public function revokeGitHubAccess()
+ {
+ try {
+ $gitHubService = $this->getService();
+
+ $basicAuthHeader = array('Authorization' => 'Basic ' .
+ base64_encode(GITHUB_CLIENT_ID.':'.GITHUB_CLIENT_SECRET));
+
+ return json_decode($gitHubService->request('/applications/'.GITHUB_CLIENT_ID.'/tokens', 'DELETE', null, $basicAuthHeader), true);
+ }
+ catch (TokenResponseException $e) {
+ return false;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php index 56739b48..db4c4a57 100644 --- a/app/Model/LastLogin.php +++ b/app/Model/LastLogin.php @@ -33,6 +33,7 @@ class LastLogin extends Base const AUTH_REMEMBER_ME = 'remember_me'; const AUTH_LDAP = 'ldap'; const AUTH_GOOGLE = 'google'; + const AUTH_GITHUB = 'github'; /** * Create a new record diff --git a/app/Model/User.php b/app/Model/User.php index 8769d69a..ba1acb90 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -52,6 +52,18 @@ class User extends Base } /** + * Get a specific user by the GitHub id + * + * @access public + * @param string $github_id GitHub user id + * @return array + */ + public function getByGitHubId($github_id) + { + return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne(); + } + + /** * Get a specific user by the username * * @access public diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 7ff130cf..a30b0989 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -3,7 +3,12 @@ namespace Schema; use Core\Security; -const VERSION = 19; +const VERSION = 20; + +function version_20($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN github_id VARCHAR(30)"); +} function version_19($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 8f8e498a..6ea0541c 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -3,7 +3,12 @@ namespace Schema; use Core\Security; -const VERSION = 19; +const VERSION = 20; + +function version_20($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN github_id TEXT"); +} function version_19($pdo) { diff --git a/app/Templates/user_edit.php b/app/Templates/user_edit.php index 6b83f748..8fba922f 100644 --- a/app/Templates/user_edit.php +++ b/app/Templates/user_edit.php @@ -48,14 +48,27 @@ <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?><br/> <?php endif ?> + <ul> <?php if (GOOGLE_AUTH && Helper\is_current_user($values['id'])): ?> + <li> <?php if (empty($values['google_id'])): ?> <a href="?controller=user&action=google<?= Helper\param_csrf() ?>"><?= t('Link my Google Account') ?></a> <?php else: ?> <a href="?controller=user&action=unlinkGoogle<?= Helper\param_csrf() ?>"><?= t('Unlink my Google Account') ?></a> <?php endif ?> + </li> <?php endif ?> + <?php if (GITHUB_AUTH && Helper\is_current_user($values['id'])): ?> + <li> + <?php if (empty($values['github_id'])): ?> + <a href="?controller=user&action=gitHub<?= Helper\param_csrf() ?>"><?= t('Link my GitHub Account') ?></a> + <?php else: ?> + <a href="?controller=user&action=unlinkGitHub<?= Helper\param_csrf() ?>"><?= t('Unlink my GitHub Account') ?></a> + <?php endif ?> + </li> + <?php endif ?> + </ul> </div> <div class="form-actions"> diff --git a/app/Templates/user_login.php b/app/Templates/user_login.php index 49902ebb..948a19e7 100644 --- a/app/Templates/user_login.php +++ b/app/Templates/user_login.php @@ -18,12 +18,20 @@ <?= Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?><br/> + <ul> <?php if (GOOGLE_AUTH): ?> - <p> + <li> <a href="?controller=user&action=google"><?= t('Login with my Google Account') ?></a> - </p> + </li> <?php endif ?> + <?php if (GITHUB_AUTH): ?> + <li> + <a href="?controller=user&action=gitHub"><?= t('Login with my GitHub Account') ?></a> + </li> + <?php endif ?> + </ul> + <div class="form-actions"> <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/> </div> diff --git a/app/common.php b/app/common.php index 5a26860f..350e70b7 100644 --- a/app/common.php +++ b/app/common.php @@ -51,6 +51,11 @@ defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false); defined('GOOGLE_CLIENT_ID') or define('GOOGLE_CLIENT_ID', ''); defined('GOOGLE_CLIENT_SECRET') or define('GOOGLE_CLIENT_SECRET', ''); +// GitHub authentication +defined('GITHUB_AUTH') or define('GITHUB_AUTH', false); +defined('GITHUB_CLIENT_ID') or define('GITHUB_CLIENT_ID', ''); +defined('GITHUB_CLIENT_SECRET') or define('GITHUB_CLIENT_SECRET', ''); + $loader = new Loader; $loader->execute(); diff --git a/assets/css/app.css b/assets/css/app.css index 60d37abc..b35ce05e 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -249,11 +249,21 @@ input.form-date { padding-left: 0; } +.form-column ul { + margin-top: 15px; +} + .form-login { padding-left: 20px; width: 430px; } +.form-column li, +.form-login li { + margin-left: 25px; + line-height: 25px; +} + /* alerts */ .alert { padding: 8px 35px 8px 14px; diff --git a/config.default.php b/config.default.php index 6810ce9d..9c12e5d2 100644 --- a/config.default.php +++ b/config.default.php @@ -43,3 +43,12 @@ define('GOOGLE_CLIENT_ID', ''); // Google client secret key (Get this value from the Google developer console) define('GOOGLE_CLIENT_SECRET', ''); + +// Enable/disable GitHub authentication +define('GITHUB_AUTH', false); + +// GitHub client id (Copy it from your settings -> Applications -> Developer applications) +define('GITHUB_CLIENT_ID', ''); + +// GitHub client secret key (Copy it from your settings -> Applications -> Developer applications) +define('GITHUB_CLIENT_SECRET', ''); diff --git a/docs/github-authentication.markdown b/docs/github-authentication.markdown new file mode 100644 index 00000000..ef99bb80 --- /dev/null +++ b/docs/github-authentication.markdown @@ -0,0 +1,62 @@ +GitHub Authentication
+=====================
+
+Requirements
+------------
+
+- OAuth GitHub API credentials (available in your [Settings > Applications > Developer applications](https://github.com/settings/applications))
+
+How does this work?
+-------------------
+
+The GitHub authentication in Kanboard uses the [OAuth 2.0](http://oauth.net/2/) protocol, so any user of Kanboard can be linked to a GitHub account. When that is done, they no longer need to manually login with their Kanboard account, but can simply automatically login with their GitHub account.
+
+How to link a GitHub account
+----------------------------------
+
+1. Login to Kanboard with the desired user
+2. Go to the **Edit user** page and click on the link **Link my GitHub Account**
+3. You are redirected to the GitHub **Authorize application** form, authorize Kanboard by clicking on the button **Accept**
+4. Finally, you are redirected to Kanboard and now your user account is linked to your GitHub account
+5. During the process, Kanboard has updated your full name and your email address based on your GitHub profile, if either of those are publically available
+6. Log out of Kanboard and you should be able to login directly with GitHub by clicking on the link **Login with my GitHub Account**
+
+Installation instructions
+-------------------------
+
+### Setting up OAuth 2.0
+
+If you know what you're doing, you can directly go to the ["Register a new OAuth application"](https://github.com/settings/applications/new) site, set everything up and skip to [Setting up Kanboard](#setting-up-kanboard) below.
+
+Summarizing the [official GitHub documentation](https://developer.github.com/guides/basics-of-authentication/#registering-your-app):
+
+- Open your [**Settings**](https://github.com/settings), select [**Applications**](https://github.com/settings/applications) from the sidebar and click on [**Register new application**](https://github.com/settings/applications/new) on the top, next to where it says **Developer applications**
+- Fill out the form with whatever values you like, only the **Authorization callback URL** _must_ be: **http://YOUR_SERVER/?controller=user&action=gitHub**
+
+### Setting up Kanboard
+
+Either create a new `config.php` file or copy and rename the `config.default.php` file and set the following values:
+
+```php
+// Enable/disable GitHub authentication
+define('GITHUB_AUTH', true);
+
+// GitHub client id (Copy it from your settings -> Applications -> Developer applications)
+define('GITHUB_CLIENT_ID', 'YOUR_GITHUB_CLIENT_ID');
+
+// GitHub client secret key (Copy it from your settings -> Applications -> Developer applications)
+define('GITHUB_CLIENT_SECRET', 'YOUR_GITHUB_CLIENT_SECRET');
+
+```
+
+Notes
+-----
+**Important:** _*Never*_ store your GITHUB_CLIENT_ID or GITHUB_CLIENT_SECRET in GitHub or somewhere with full public access in general!
+
+Kanboard uses these information from your public GitHub profile:
+
+- Full name
+- Public email address
+- GitHub unique id
+
+The GitHub unique id is used to link the local user account and the GitHub account.
|