├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── images ├── app-demo.png └── app-registration.png ├── index.html ├── jsconfig.json ├── main-config.js ├── main.js ├── menu.js ├── package.json ├── src ├── app │ ├── app.component.ts │ ├── app.module.ts │ ├── main.ts │ └── view-main.html ├── auth │ └── auth-guard.service.ts ├── config │ └── azure-config.ts ├── contacts │ ├── contacts.component.ts │ └── view-contacts.html ├── files │ ├── files.component.ts │ └── view-files.html ├── groups │ ├── groups.component.ts │ └── view-groups.html ├── home │ ├── home.component.ts │ └── view-home.html ├── login │ ├── login.component.ts │ └── view-login.html ├── mails │ ├── mails.component.ts │ └── view-mails.html ├── messages │ └── messages.ts ├── models │ └── runtime-info.model.ts ├── notes │ ├── notes.component.ts │ └── view-notes.html ├── office │ └── office-urls.ts ├── package.json ├── profile │ └── profile.component.ts ├── routes │ └── app.routes.ts ├── services │ ├── electron.service.ts │ └── graph.service.ts ├── tasks │ ├── tasks.component.ts │ └── view-tasks.html ├── toast │ └── toast.component.ts ├── trending │ ├── trending.component.ts │ └── view-trending.html └── users │ ├── users.component.ts │ └── view-users.html ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | build 29 | typings 30 | releases 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cecil du Toit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular2-O365-desktop-app 2 | Desktop application using Electron, Angular 2, Material Design Lite and Office 365 (Microsoft Graph API). 3 | 4 | ![Application Demo](./images/app-demo.png) 5 | 6 | ## Getting started 7 | 8 | Install dependencies 9 | 10 | ` 11 | npm install 12 | ` 13 | 14 | To build and watch for changes 15 | 16 | ` 17 | npm run watch 18 | ` 19 | 20 | Run application 21 | 22 | ` 23 | npm start 24 | ` 25 | 26 | ## Debug Electron using [Devtron](http://electron.atom.io/devtron/) 27 | 28 | ` 29 | 30 | // Run the following from the Console tab of your app's DevTools 31 | 32 | require('devtron').install() 33 | 34 | // You should now see a Devtron tab added to the DevTools 35 | ` 36 | 37 | ## Azure AD application Registration 38 | 39 | Register this application in the Azure Active Directory service for your Office 365 tenant. 40 | Easy to use online tool: [Office 365 App Registration Tool](https://dev.office.com/app-registration) 41 | 42 | * App Registration Type: Native Application 43 | * Redirect URI: http://localhost/callback 44 | * Select all read items for Users, Groups, Mail, Calendar, Contacts and Files. 45 | * Register the app and remember to copy your client ID provided to you. 46 | * Update the azure-config.ts file with your values. 47 | 48 | ![Application Registration](./images/app-registration.png) 49 | 50 | Once your app is registered you need to download the app manifest file from [Azure Portal](https://manage.windowsazure.com/) 51 | Set "oauth2AllowImplicitFlow": true in application manifest file and upload the manifest file again. 52 | 53 | ### Azure AD Application Permission requirements 54 | You need to make sure the application permission is set correctly to have access to the Microsoft Graph resource. 55 | This can be done in the [Azure Portal](https://manage.windowsazure.com/). 56 | 57 | Required Permissions 58 | 59 | * Read all notebooks that the user can access 60 | * Access user's data anytime 61 | * Read items in all site collections 62 | * Read files that the user selects 63 | * Read user files and files shared with user 64 | * Read user contacts 65 | * Read user calendars 66 | * Read user mail 67 | * Read directory data 68 | * Read all groups 69 | * Read all users' full profiles 70 | * Read all users' basic profiles 71 | * Sign in and read user profile 72 | * Read user notebooks 73 | * Read users' relevant people list 74 | 75 | Go to [Microsoft Graph API](http://graph.microsoft.io/docs/overview/overview) if you want to know more. 76 | 77 | ## Application Build & Deployment 78 | 79 | Follow Electron documentation for build for Windows, Linux and OSX. 80 | 81 | To package files for deployment: 82 | 83 | 84 | * npm install -g asar 85 | * asar pack angular2-O365-desktop-app app.asar 86 | 87 | TODO Packaging: https://www.xplatform.rocks/2016/02/14/angular2-and-electron-the-definitive-guide/ 88 | 89 | 90 | ** More details to follow ** 91 | 92 | ## Resources 93 | 94 | * [Angular 2](https://angular.io/) 95 | * [Material Design Lite](http://www.getmdl.io/) 96 | * [Electron](http://electron.atom.io/) 97 | * [Office 365](https://products.office.com/en-gb/business/compare-office-365-for-business-plans) 98 | * [Office 365 Development](https://dev.office.com/) 99 | 100 | If you have any issues or suggestion please submit on [GitHub Issues](https://github.com/Cecildt/angular2-O365-desktop-app/issues). -------------------------------------------------------------------------------- /images/app-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cecildt/angular2-O365-desktop-app/543815bd35a268ede6c8a9758748e51b530d836f/images/app-demo.png -------------------------------------------------------------------------------- /images/app-registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cecildt/angular2-O365-desktop-app/543815bd35a268ede6c8a9758748e51b530d836f/images/app-registration.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Office 365 Desktop Experience 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Loading... 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "bower_components", 12 | "jspm_packages", 13 | "tmp", 14 | "temp" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /main-config.js: -------------------------------------------------------------------------------- 1 | var mainConfig = { 2 | authorityHostUrl: 'https://login.windows.net', 3 | tenant: 'mod340807.onmicrosoft.com', 4 | clientId: 'bbffe1fd-bf52-41b2-b898-4903cb73f9db', 5 | clientSecret: '', 6 | extraQueryParameter: 'nux=1', 7 | disableRenewal: true, 8 | resource: "https://graph.microsoft.com", 9 | redirectUri: "/", 10 | postLogoutRedirectUri: "", 11 | endpoints: { 12 | graphApiUri: 'https://graph.microsoft.com' 13 | } 14 | } 15 | 16 | module.export = mainConfig; -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const electron = require('electron'); 4 | const app = electron.app; 5 | 6 | const AdalMainConfig = require("./main-config"); 7 | 8 | require('electron-debug')({ showDevTools: true }); 9 | 10 | // browser-window creates a native window 11 | const BrowserWindow = electron.BrowserWindow; 12 | let mainWindow = null; 13 | let hash = ""; 14 | 15 | // Allows for live-reload while developing the app 16 | require('electron-reload')(__dirname + '/build'); 17 | 18 | function createWindow() { 19 | 20 | // Initialize the window to our specified dimensions 21 | mainWindow = new BrowserWindow({ 22 | width: 1200, 23 | height: 900, 24 | autoHideMenuBar: true, 25 | webPreferences: { 26 | nodeIntegration: true 27 | } 28 | }); 29 | 30 | mainWindow.maximize(); 31 | // Tell Electron where to load the entry point from 32 | mainWindow.loadURL('file://' + __dirname + '/index.html'); 33 | 34 | // Clear out the main window when the app is closed 35 | mainWindow.on('closed', function () { 36 | mainWindow = null; 37 | }); 38 | } 39 | 40 | app.on('ready', createWindow); 41 | 42 | app.on('window-all-closed', function () { 43 | if (process.platform != 'darwin') { 44 | app.quit(); 45 | } 46 | }); 47 | 48 | app.on('activate', function () { 49 | // On OS X it's common to re-create a window in the app when the 50 | // dock icon is clicked and there are no other windows open. 51 | if (mainWindow === null) { 52 | createWindow(); 53 | } 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /menu.js: -------------------------------------------------------------------------------- 1 | const remote = require('electron').remote; 2 | const Menu = remote.Menu; 3 | 4 | var template = [ 5 | { 6 | label: 'Edit', 7 | submenu: [ 8 | { 9 | label: 'Undo', 10 | accelerator: 'CmdOrCtrl+Z', 11 | role: 'undo' 12 | }, 13 | { 14 | label: 'Redo', 15 | accelerator: 'Shift+CmdOrCtrl+Z', 16 | role: 'redo' 17 | }, 18 | { 19 | type: 'separator' 20 | }, 21 | { 22 | label: 'Cut', 23 | accelerator: 'CmdOrCtrl+X', 24 | role: 'cut' 25 | }, 26 | { 27 | label: 'Copy', 28 | accelerator: 'CmdOrCtrl+C', 29 | role: 'copy' 30 | }, 31 | { 32 | label: 'Paste', 33 | accelerator: 'CmdOrCtrl+V', 34 | role: 'paste' 35 | }, 36 | { 37 | label: 'Select All', 38 | accelerator: 'CmdOrCtrl+A', 39 | role: 'selectall' 40 | }, 41 | { 42 | label: 'Custom Action', 43 | role: 'customAction' 44 | } 45 | ] 46 | }, 47 | { 48 | label: 'View', 49 | submenu: [ 50 | { 51 | label: 'Reload', 52 | accelerator: 'CmdOrCtrl+R', 53 | click: function(item, focusedWindow) { 54 | if (focusedWindow) 55 | focusedWindow.reload(); 56 | } 57 | }, 58 | { 59 | label: 'Toggle Full Screen', 60 | accelerator: (function() { 61 | if (process.platform == 'darwin') 62 | return 'Ctrl+Command+F'; 63 | else 64 | return 'F11'; 65 | })(), 66 | click: function(item, focusedWindow) { 67 | if (focusedWindow) 68 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 69 | } 70 | }, 71 | { 72 | label: 'Toggle Developer Tools', 73 | accelerator: (function() { 74 | if (process.platform == 'darwin') 75 | return 'Alt+Command+I'; 76 | else 77 | return 'Ctrl+Shift+I'; 78 | })(), 79 | click: function(item, focusedWindow) { 80 | if (focusedWindow) 81 | focusedWindow.webContents.toggleDevTools(); 82 | } 83 | }, 84 | ] 85 | }, 86 | { 87 | label: 'Window', 88 | role: 'window', 89 | submenu: [ 90 | { 91 | label: 'Minimize', 92 | accelerator: 'CmdOrCtrl+M', 93 | role: 'minimize' 94 | }, 95 | { 96 | label: 'Close', 97 | accelerator: 'CmdOrCtrl+W', 98 | role: 'close' 99 | }, 100 | ] 101 | }, 102 | { 103 | label: 'Help', 104 | role: 'help', 105 | submenu: [ 106 | { 107 | label: 'Electron', 108 | click: function() { require('electron').shell.openExternal('http://electron.atom.io') } 109 | }, 110 | { 111 | label: 'Office 365', 112 | click: function() { require('electron').shell.openExternal('https://office365.com') } 113 | }, 114 | { 115 | label: 'Office 365 Development', 116 | click: function() { require('electron').shell.openExternal('https://dev.office.com') } 117 | }, 118 | { 119 | label: 'Office Graph', 120 | click: function() { require('electron').shell.openExternal('https://graph.microsoft.com') } 121 | } 122 | ] 123 | }, 124 | ]; 125 | 126 | if (process.platform == 'darwin') { 127 | var name = require('electron').remote.app.getName(); 128 | template.unshift({ 129 | label: name, 130 | submenu: [ 131 | { 132 | label: 'About ' + name, 133 | role: 'about' 134 | }, 135 | { 136 | type: 'separator' 137 | }, 138 | { 139 | label: 'Services', 140 | role: 'services', 141 | submenu: [] 142 | }, 143 | { 144 | type: 'separator' 145 | }, 146 | { 147 | label: 'Hide ' + name, 148 | accelerator: 'Command+H', 149 | role: 'hide' 150 | }, 151 | { 152 | label: 'Hide Others', 153 | accelerator: 'Command+Alt+H', 154 | role: 'hideothers' 155 | }, 156 | { 157 | label: 'Show All', 158 | role: 'unhide' 159 | }, 160 | { 161 | type: 'separator' 162 | }, 163 | { 164 | label: 'Quit', 165 | accelerator: 'Command+Q', 166 | click: function() { app.quit(); } 167 | }, 168 | ] 169 | }); 170 | // Window menu. 171 | template[3].submenu.push( 172 | { 173 | type: 'separator' 174 | }, 175 | { 176 | label: 'Bring All to Front', 177 | role: 'front' 178 | } 179 | ); 180 | } 181 | 182 | var menu = Menu.buildFromTemplate(template); 183 | Menu.setApplicationMenu(menu); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-o365-desktop-app", 3 | "version": "1.0.0", 4 | "description": "Angular 2 desktop application using Electron and Office 365.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Cecildt/angular2-O365-desktop-app.git" 8 | }, 9 | "eslintConfig": { 10 | "env": { 11 | "browser": true, 12 | "node": true 13 | } 14 | }, 15 | "main": "main.js", 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "start": "electron main.js", 19 | "build": "webpack --progress --profile --colors --display-error-details --display-cached", 20 | "watch": "webpack-dashboard -- webpack --watch --progress --profile --colors --display-error-details --display-cached", 21 | "distribute": "electron-packager . O365-Angular --platform=darwin --arch=x64 --out=releases/ --overwrite --icon=src/assets/images/icon --asar", 22 | "electron:package": "npm run electron:pack-mac && npm run electron:pack-win && npm run electron:pack-linux", 23 | "electron:pack-mac": "electron-packager www --asar --overwrite --platform=darwin --arch=x64 --out=built", 24 | "electron:pack-win": "electron-packager www --asar --overwrite --platform=win32 --arch=ia32 --out=built", 25 | "electron:pack-linux": "electron-packager www --asar --overwrite --platform=linux --arch=x64 --out=built" 26 | }, 27 | "author": "Cecil du Toit", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@angular/common": "2.4.0", 31 | "@angular/compiler": "2.4.0", 32 | "@angular/core": "2.4.0", 33 | "@angular/http": "2.4.0", 34 | "@angular/platform-browser": "2.4.0", 35 | "@angular/platform-browser-dynamic": "2.4.0", 36 | "@angular/router": "3.4.0", 37 | "core-js": "^2.4.1", 38 | "material-design-lite": "^1.3.0", 39 | "modular-adal-angular": "^1.0.4", 40 | "oauth": "^0.9.14", 41 | "reflect-metadata": "^0.1.9", 42 | "rxjs": "5.0.1", 43 | "systemjs": "^0.19.40", 44 | "zone.js": "^0.7.4" 45 | }, 46 | "devDependencies": { 47 | "@types/core-js": "^0.9.35", 48 | "@types/electron": "^1.4.30", 49 | "@types/lodash": "^4.14.45", 50 | "@types/node": "^6.0.56", 51 | "@types/rx": "^4.1.0", 52 | "@types/rx-angular": "0.0.28", 53 | "@types/webpack": "^2.1.0", 54 | "@types/zone.js": "0.0.27", 55 | "codelyzer": "^2.0.0-beta.4", 56 | "css-loader": "^0.26.1", 57 | "devtron": "^1.4.0", 58 | "electron-debug": "^1.1.0", 59 | "electron-packager": "^8.4.0", 60 | "electron-prebuilt": "^1.4.13", 61 | "electron-reload": "^1.1.0", 62 | "expose-loader": "^0.7.1", 63 | "file-loader": "^0.9.0", 64 | "json-loader": "^0.5.4", 65 | "raw-loader": "^0.5.1", 66 | "ts-loader": "^1.3.3", 67 | "tslint": "^4.2.0", 68 | "typescript": "^2.1.4", 69 | "url-loader": "^0.5.7", 70 | "webpack": "^1.14.0", 71 | "webpack-dashboard": "^0.2.1", 72 | "webpack-dev-server": "^1.16.2" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, AfterViewInit, OnInit } from "@angular/core"; 2 | 3 | import { ElectronService } from "../services/electron.service"; 4 | import { RuntimeInfoModel } from "../models/runtime-info.model"; 5 | 6 | declare var componentHandler: any; 7 | 8 | @Component({ 9 | providers: [ElectronService], 10 | selector: "graph-app", 11 | templateUrl: "src/app/view-main.html", 12 | }) 13 | export class AppComponent implements AfterViewInit, OnInit { 14 | private nodeVersion: string = ""; 15 | private chromeVersion: string = ""; 16 | private electronVersion: string = ""; 17 | 18 | constructor(private electronService: ElectronService) { 19 | } 20 | 21 | public ngOnInit() { 22 | let info: RuntimeInfoModel = this.electronService.getInfo(); 23 | 24 | this.nodeVersion = info.nodeVersion; 25 | this.chromeVersion = info.chromeVersion; 26 | this.electronVersion = info.electronVersion; 27 | } 28 | 29 | public ngAfterViewInit() { 30 | componentHandler.upgradeAllRegistered(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { BrowserModule } from "@angular/platform-browser"; 3 | import { HttpModule } from "@angular/http"; 4 | 5 | import { APP_ROUTING } from "../routes/app.routes"; 6 | import { AppComponent } from "../app/app.component"; 7 | import { GraphService } from "../services/graph.service"; 8 | import { ProfileComponent } from "../profile/profile.component"; 9 | import { ElectronService } from "../services/electron.service"; 10 | import { ToastComponent } from "../toast/toast.component"; 11 | import { HomeComponent } from "../home/home.component"; 12 | import { LoginComponent } from "../login/login.component"; 13 | import { FilesComponent } from "../files/files.component"; 14 | import { ContactsComponent } from "../contacts/contacts.component"; 15 | import { GroupsComponent } from "../groups/groups.component"; 16 | import { MailsComponent } from "../mails/mails.component"; 17 | import { NotesComponent } from "../notes/notes.component"; 18 | import { TasksComponent } from "../tasks/tasks.component"; 19 | import { TrendingComponent } from "../trending/trending.component"; 20 | import { UsersComponent } from "../users/users.component"; 21 | import { AuthGuardService } from "../auth/auth-guard.service"; 22 | 23 | @NgModule({ 24 | bootstrap: [ AppComponent ], 25 | declarations: [ 26 | AppComponent, ToastComponent, ProfileComponent, HomeComponent, LoginComponent, FilesComponent, 27 | ContactsComponent, GroupsComponent, MailsComponent, NotesComponent, TasksComponent, TrendingComponent, 28 | UsersComponent ], 29 | imports: [ BrowserModule, HttpModule, APP_ROUTING ], 30 | providers: [ GraphService, AuthGuardService, ElectronService, ProfileComponent, ToastComponent ], 31 | }) 32 | export class AppModule { } 33 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | const platform = platformBrowserDynamic(); 6 | platform.bootstrapModule(AppModule); -------------------------------------------------------------------------------- /src/app/view-main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 39 |
40 |
41 | Menu 42 | 71 |
72 |
73 |
74 | 75 |
76 |
77 | 78 |
79 |
80 | 91 | 92 |
-------------------------------------------------------------------------------- /src/auth/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { 3 | CanActivate, 4 | Router, 5 | ActivatedRouteSnapshot, 6 | RouterStateSnapshot, 7 | } from "@angular/router"; 8 | 9 | import { GraphService } from "../services/graph.service"; 10 | import { ToastComponent } from "../toast/toast.component"; 11 | import { USER_MESSAGES } from "../messages/messages"; 12 | 13 | @Injectable() 14 | export class AuthGuardService implements CanActivate { 15 | constructor(private graph: GraphService, private toast: ToastComponent, private router: Router) {} 16 | 17 | public canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { 18 | return this.graph.isUserAuthenticated().then(() => { 19 | return true; 20 | }).catch((err) => { 21 | this.toast.show(USER_MESSAGES.not_authenticated + " " + err); 22 | this.router.navigate(["login"]); 23 | return false; 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/config/azure-config.ts: -------------------------------------------------------------------------------- 1 | export const AZURE_CONFIG = { 2 | authorityHostUrl: "https://login.windows.net", 3 | clientId: "8582721b-acf4-4f1e-b1a3-6be84a8b55f7", 4 | disableRenewal: true, 5 | endpoints: { 6 | graphApiUri: "https://graph.microsoft.com", 7 | }, 8 | extraQueryParameter: "nux=1", 9 | postLogoutRedirectUri: "", 10 | redirectUri: "http://localhost/callback", 11 | resource: "https://graph.microsoft.com", 12 | tenant: "devlaundry.onmicrosoft.com", 13 | }; 14 | -------------------------------------------------------------------------------- /src/contacts/contacts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-contacts", 10 | templateUrl: "src/contacts/view-contacts.html", 11 | }) 12 | export class ContactsComponent implements OnInit { 13 | private contacts = []; 14 | 15 | constructor(private graph: GraphService, 16 | private toast: ToastComponent, 17 | private changeRef: ChangeDetectorRef) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.toast.show(USER_MESSAGES.get_contacts); 22 | this.getContacts(); 23 | } 24 | 25 | private getContacts() { 26 | this.graph.getRequestPromise(OFFICE_URLS.me_contacts_url) 27 | .then((data: any) => { 28 | if (data) { 29 | this.contacts = data.value; 30 | this.changeRef.detectChanges(); 31 | } else { 32 | this.toast.show(USER_MESSAGES.fail_graph_api); 33 | } 34 | }) 35 | .catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/contacts/view-contacts.html: -------------------------------------------------------------------------------- 1 |

My Contacts

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 20 | 21 |
Display NameEmail
14 | {{contact.displayName}} 15 | 17 | {{contact.emailAddresses[0].address}} 18 |
-------------------------------------------------------------------------------- /src/files/files.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-files", 10 | templateUrl: "src/files/view-files.html", 11 | }) 12 | 13 | export class FilesComponent implements OnInit { 14 | private files; 15 | 16 | constructor(private graph: GraphService, 17 | private toast: ToastComponent, 18 | private changeRef: ChangeDetectorRef) { 19 | } 20 | 21 | public ngOnInit() { 22 | this.toast.show(USER_MESSAGES.get_files); 23 | this.getFiles(); 24 | } 25 | 26 | public getFiles() { 27 | this.graph.getRequestPromise(OFFICE_URLS.me_drive_url) 28 | .then((data: any) => { 29 | if (data) { 30 | this.files = data.value; 31 | this.changeRef.detectChanges(); 32 | } else { 33 | this.toast.show(USER_MESSAGES.fail_graph_api); 34 | } 35 | }).catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api); 37 | }); 38 | } 39 | 40 | public refreshInfo() { 41 | this.getFiles(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/files/view-files.html: -------------------------------------------------------------------------------- 1 |

My Files

2 |
3 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 25 | 26 | 27 | 28 |
TypeNameSize
{{i + 1}} 19 | File 20 | Folder 21 | 23 | {{file.name}} 24 | {{file.size}}
-------------------------------------------------------------------------------- /src/groups/groups.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-groups", 10 | templateUrl: "src/groups/view-groups.html", 11 | }) 12 | export class GroupsComponent implements OnInit { 13 | private groups = []; 14 | 15 | constructor(public graph: GraphService, 16 | public toast: ToastComponent, 17 | private changeRef: ChangeDetectorRef) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.toast.show(USER_MESSAGES.get_groups); 22 | this.getGroups(); 23 | } 24 | 25 | private getGroups() { 26 | this.graph.getRequestPromise(OFFICE_URLS.me_groups_url) 27 | .then((data: any) => { 28 | if (data) { 29 | this.groups = data.value; 30 | this.changeRef.detectChanges(); 31 | } else { 32 | this.toast.show(USER_MESSAGES.fail_graph_api); 33 | } 34 | }) 35 | .catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/groups/view-groups.html: -------------------------------------------------------------------------------- 1 |

My Groups

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 22 | 23 |
NameMail AddressVisibility
15 | {{group.displayName}} 16 | 18 | {{group.mail}} 19 | {{group.visibility}}
-------------------------------------------------------------------------------- /src/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | import { ProfileComponent } from "../profile/profile.component"; 4 | import { ElectronService } from "../services/electron.service"; 5 | 6 | @Component({ 7 | selector: "my-home", 8 | templateUrl: "src/home/view-home.html", 9 | }) 10 | export class HomeComponent { 11 | 12 | constructor(private electron: ElectronService, 13 | private profile: ProfileComponent) { 14 | } 15 | 16 | public signOut() { 17 | this.electron.logOut(); 18 | } 19 | 20 | public refreshInfo() { 21 | this.profile.refreshInfo(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/home/view-home.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 |

8 | Information is only read from your Office 365 tenant. This application does not modify any information. Also does not provide any functionality to modify information. 9 |

10 |
11 | 12 |
-------------------------------------------------------------------------------- /src/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | import { ElectronService } from "../services/electron.service"; 4 | 5 | @Component({ 6 | selector: "my-login", 7 | templateUrl: "src/login/view-login.html", 8 | }) 9 | export class LoginComponent { 10 | constructor(private electronService: ElectronService) { 11 | } 12 | 13 | public login() { 14 | // Clear local cache 15 | window.localStorage.removeItem("id_token"); 16 | window.localStorage.removeItem("access_token"); 17 | window.localStorage.removeItem("user"); 18 | 19 | this.electronService.logIn(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/login/view-login.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mails/mails.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-mails", 10 | templateUrl: "src/mails/view-mails.html", 11 | }) 12 | export class MailsComponent implements OnInit { 13 | private messages = []; 14 | 15 | constructor(private graph: GraphService, 16 | private toast: ToastComponent, 17 | private changeRef: ChangeDetectorRef) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.toast.show(USER_MESSAGES.get_mails); 22 | this.getMails(); 23 | } 24 | 25 | private getMails() { 26 | this.graph.getRequestPromise(OFFICE_URLS.me_messages_url) 27 | .then((data: any) => { 28 | if (data) { 29 | this.messages = data.value; 30 | this.changeRef.detectChanges(); 31 | } else { 32 | this.toast.show(USER_MESSAGES.fail_graph_api); 33 | } 34 | }) 35 | .catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/mails/view-mails.html: -------------------------------------------------------------------------------- 1 |

My Mails

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 24 | 27 | 28 | 29 |
SubjectSend DateHas AttachmentsBody Preview
16 | {{message.subject}} 17 | 19 | {{message.sendDateTime}} 20 | 22 | {{message.hasAttachments}} 23 | 25 | {{message.bodyPreview}} 26 |
-------------------------------------------------------------------------------- /src/messages/messages.ts: -------------------------------------------------------------------------------- 1 | export const USER_MESSAGES = { 2 | fail_graph_api: "Failed to the Microsoft Graph API.", 3 | fail_user_photo: "Failed to get your photo!", 4 | fail_username: "Failed to get your name!", 5 | get_contacts: "Getting your contacts...", 6 | get_files: "Getting your files...", 7 | get_groups: "Getting your groups...", 8 | get_mails: "Getting your mails...", 9 | get_notebooks: "Getting your notebooks...", 10 | get_tasks: "Getting your tasks...", 11 | get_trending: "Getting your trending content...", 12 | get_users: "Getting your organization users...", 13 | no_access_token: "No access token available. Please login.", 14 | not_authenticated: "You are not authenticated!", 15 | redirect_login: "Redirecting to Login. You must first login to Office 365.", 16 | }; 17 | -------------------------------------------------------------------------------- /src/models/runtime-info.model.ts: -------------------------------------------------------------------------------- 1 | export class RuntimeInfoModel{ 2 | constructor(public nodeVersion: string, public chromeVersion: string, public electronVersion: string){ 3 | } 4 | } -------------------------------------------------------------------------------- /src/notes/notes.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-notes", 10 | templateUrl: "src/notes/view-notes.html", 11 | }) 12 | export class NotesComponent implements OnInit { 13 | private books = []; 14 | 15 | constructor(private graph: GraphService, 16 | private toast: ToastComponent, 17 | private changeRef: ChangeDetectorRef) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.toast.show(USER_MESSAGES.get_notebooks); 22 | this.getNotebooks(); 23 | } 24 | 25 | private getNotebooks() { 26 | this.graph.getRequestPromise(OFFICE_URLS.me_notebooks_url) 27 | .then((data: any) => { 28 | if (data) { 29 | this.books = data.value; 30 | this.changeRef.detectChanges(); 31 | } else { 32 | this.toast.show(USER_MESSAGES.fail_graph_api); 33 | } 34 | }) 35 | .catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/notes/view-notes.html: -------------------------------------------------------------------------------- 1 |

My Notes

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 22 | 23 | 24 |
NameLast Modified ByLast Modified Date
14 | {{book.name}} 15 | 17 | {{book.lastModifiedBy}} 18 | 20 | {{book.lastModifiedTime}} 21 |
-------------------------------------------------------------------------------- /src/office/office-urls.ts: -------------------------------------------------------------------------------- 1 | export const OFFICE_URLS = { 2 | all_users_url: "/v1.0/users", 3 | me_contacts_url: "/v1.0/me/contacts", 4 | me_drive_url: "/v1.0/me/drive/root/children", 5 | me_groups_url: "/v1.0/me/memberOf/$/microsoft.graph.group", 6 | me_messages_url: "/v1.0/me/messages", 7 | me_notebooks_url: "/beta/me/notes/notebooks", 8 | me_photo_url: "/v1.0/me/photo/$value", 9 | me_profile_url: "/v1.0/me/", 10 | me_tasks_url: "/beta/me/tasks", 11 | me_trending_around_url: "/beta/me/trendingAround", 12 | }; 13 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-o365-desktop-app", 3 | "version": "0.0.1", 4 | "main": "main.js" 5 | } -------------------------------------------------------------------------------- /src/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injectable, ApplicationRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-profile", 10 | template: "
Welcome {{displayName}}
", 11 | }) 12 | @Injectable() 13 | export class ProfileComponent implements OnInit { 14 | private displayName: string = "User"; 15 | private photo: string = ""; 16 | 17 | constructor(private graph: GraphService, 18 | private toast: ToastComponent, 19 | private changeRef: ApplicationRef) { 20 | } 21 | 22 | public ngOnInit() { 23 | this.refreshInfo(); 24 | } 25 | 26 | public refreshInfo() { 27 | 28 | this.graph.isUserAuthenticated() 29 | .then(() => { 30 | this.getUserName(); 31 | this.getUserPhoto(); 32 | }) 33 | .catch((reason) => { 34 | this.toast.show(USER_MESSAGES.not_authenticated); 35 | }); 36 | } 37 | 38 | private getUserName() { 39 | this.graph.getRequestPromise(OFFICE_URLS.me_profile_url).then((data: any) => { 40 | if (data) { 41 | this.displayName = data.displayName; 42 | this.changeRef.tick(); 43 | } else { 44 | this.toast.show(USER_MESSAGES.fail_username); 45 | } 46 | }).catch(() => { 47 | this.toast.show(USER_MESSAGES.fail_graph_api); 48 | }); 49 | } 50 | 51 | private getUserPhoto() { 52 | this.graph.getPhotoRequestPromise(OFFICE_URLS.me_photo_url).then((data: any) => { 53 | if (data) { 54 | this.photo = data; 55 | this.changeRef.tick(); 56 | } else { 57 | this.toast.show(USER_MESSAGES.fail_user_photo); 58 | } 59 | }).catch(() => { 60 | this.toast.show(USER_MESSAGES.fail_graph_api); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/routes/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | 4 | import { HomeComponent } from "../home/home.component"; 5 | import { LoginComponent } from "../login/login.component"; 6 | import { FilesComponent } from "../files/files.component"; 7 | import { ContactsComponent } from "../contacts/contacts.component"; 8 | import { GroupsComponent } from "../groups/groups.component"; 9 | import { MailsComponent } from "../mails/mails.component"; 10 | import { NotesComponent } from "../notes/notes.component"; 11 | import { TasksComponent } from "../tasks/tasks.component"; 12 | import { TrendingComponent } from "../trending/trending.component"; 13 | import { UsersComponent } from "../users/users.component"; 14 | import { AuthGuardService } from "../auth/auth-guard.service"; 15 | 16 | const appRoutes: Routes = [ 17 | { component: LoginComponent, path: "login" }, 18 | { canActivate: [AuthGuardService], component: HomeComponent, path: "" }, 19 | { canActivate: [AuthGuardService], component: HomeComponent, path: "home" }, 20 | { canActivate: [AuthGuardService], component: FilesComponent, path: "files" }, 21 | { canActivate: [AuthGuardService], component: GroupsComponent, path: "groups" }, 22 | { canActivate: [AuthGuardService], component: ContactsComponent, path: "contacts" }, 23 | { canActivate: [AuthGuardService], component: MailsComponent, path: "mails" }, 24 | { canActivate: [AuthGuardService], component: NotesComponent, path: "notes" }, 25 | { canActivate: [AuthGuardService], component: TasksComponent, path: "tasks" }, 26 | { canActivate: [AuthGuardService], component: TrendingComponent, path: "trending" }, 27 | { canActivate: [AuthGuardService], component: UsersComponent, path: "users" }, 28 | { component: LoginComponent, path: "**"}, 29 | ]; 30 | 31 | export const APP_ROUTING: ModuleWithProviders = RouterModule.forRoot(appRoutes); 32 | -------------------------------------------------------------------------------- /src/services/electron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Http } from "@angular/http"; 3 | import { Router } from "@angular/router"; 4 | import "rxjs/add/operator/toPromise"; 5 | import { remote } from "electron"; 6 | 7 | import { RuntimeInfoModel } from "../models/runtime-info.model"; 8 | import { AZURE_CONFIG } from "../config/azure-config"; 9 | 10 | @Injectable() 11 | export class ElectronService { 12 | 13 | private originalURL; 14 | 15 | constructor(private http: Http, private router: Router) { 16 | } 17 | 18 | public getInfo(): RuntimeInfoModel { 19 | return new RuntimeInfoModel( 20 | remote.process.versions.node, 21 | remote.process.versions.chrome, 22 | remote.process.versions.electron); 23 | } 24 | 25 | public logIn(state = "/") { 26 | this.originalURL = location.href; 27 | let authUrl = "https://login.microsoftonline.com/" + AZURE_CONFIG.tenant + 28 | "/oauth2/authorize?response_type=id_token&client_id=" + AZURE_CONFIG.clientId + 29 | "&redirect_uri=" + encodeURIComponent(AZURE_CONFIG.redirectUri) + 30 | "&state=" + state + "&nonce=SomeNonce"; 31 | let BrowserWindow = remote.BrowserWindow; 32 | 33 | let authWindow = new BrowserWindow({ 34 | autoHideMenuBar: true, 35 | frame: true, 36 | height: 600, 37 | show: false, 38 | webPreferences: { 39 | nodeIntegration: false, 40 | }, 41 | width: 800, 42 | }); 43 | 44 | authWindow.webContents.on("did-get-redirect-request", (event: any, oldUrl: string, newUrl: string) => { 45 | authWindow.destroy(); 46 | let params: any = this.parseQueryString(newUrl); 47 | let idToken = params.id_token; 48 | if (idToken != null) { 49 | window.localStorage.setItem("id_token", idToken); 50 | window.localStorage.removeItem("access_token"); 51 | 52 | this.getNewAccessToken(); 53 | } else { 54 | window.localStorage.removeItem("id_token"); 55 | window.localStorage.removeItem("access_token"); 56 | } 57 | }); 58 | 59 | authWindow.on("closed", () => { 60 | authWindow = null; 61 | }); 62 | 63 | authWindow.loadURL(authUrl); 64 | authWindow.show(); 65 | } 66 | 67 | public getNewAccessToken(state = "/") { 68 | let originalURL = location.href; 69 | let accessTokenUrl = "https://login.microsoftonline.com/" + AZURE_CONFIG.tenant + 70 | "/oauth2/authorize?response_type=token&client_id=" + AZURE_CONFIG.clientId + 71 | "&resource=" + AZURE_CONFIG.endpoints.graphApiUri + 72 | "&redirect_uri=" + encodeURIComponent(AZURE_CONFIG.redirectUri) + 73 | "&prompt=none&state=" + state + "&nonce=SomeNonce"; 74 | let BrowserWindow = remote.BrowserWindow; 75 | 76 | let accessWindow = new BrowserWindow({ 77 | autoHideMenuBar: true, 78 | frame: false, 79 | height: 600, 80 | show: false, 81 | webPreferences: { 82 | nodeIntegration: false, 83 | }, 84 | width: 800, 85 | }); 86 | 87 | accessWindow.on("closed", () => { 88 | accessWindow = null; 89 | }); 90 | 91 | accessWindow.webContents.on("did-get-redirect-request", (event: any, oldUrl: string, newUrl: string) => { 92 | accessWindow.destroy(); 93 | let params: any = this.parseQueryString(newUrl); 94 | let accessToken = params.access_token; 95 | if (accessToken != null) { 96 | window.localStorage.setItem("access_token", accessToken); 97 | // remote.getCurrentWindow().loadURL(originalURL + "index.html"); 98 | this.router.navigate(["home"]); 99 | } else { 100 | window.localStorage.removeItem("access_token"); 101 | } 102 | }); 103 | 104 | accessWindow.loadURL(accessTokenUrl); 105 | } 106 | 107 | public logOut(state = "/") { 108 | window.localStorage.removeItem("id_token"); 109 | window.localStorage.removeItem("access_token"); 110 | this.router.navigate(["login"]); 111 | } 112 | 113 | public refreshAccessToken(state = "/") { 114 | this.logIn(state); // force login, assume that renewToken.html didn't work which is why dev is calling this. 115 | } 116 | 117 | public getAccessToken() { 118 | return window.localStorage.getItem("access_token"); 119 | } 120 | 121 | private parseQueryString(url: string) { 122 | let params = {}; 123 | let queryString = ""; 124 | 125 | if (url.search("#") !== -1) { 126 | queryString = url.substring(url.search("#") + 1); 127 | } else { 128 | queryString = url.substring(url.indexOf("?") + 1); 129 | } 130 | 131 | let a = queryString.split("&"); 132 | for (let i = 0; i < a.length; i++) { 133 | let b = a[i].split("="); 134 | params[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ""); 135 | } 136 | 137 | return params; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/services/graph.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Http, Headers } from "@angular/http"; 3 | import "rxjs"; 4 | 5 | import { AZURE_CONFIG } from "../config/azure-config"; 6 | import { USER_MESSAGES } from "../messages/messages"; 7 | import { OFFICE_URLS } from "../office/office-urls"; 8 | 9 | @Injectable() 10 | export class GraphService { 11 | 12 | private http: Http; 13 | private accessToken: string | null; 14 | 15 | constructor(http: Http) { 16 | this.http = http; 17 | } 18 | 19 | public isUserAuthenticated = (): Promise => { 20 | let p = new Promise((resolve: Function, reject: Function) => { 21 | let token: string | null = localStorage.getItem("access_token"); 22 | this.accessToken = token; 23 | 24 | if (this.accessToken === null) { 25 | reject(USER_MESSAGES.no_access_token); 26 | } 27 | 28 | this.getRequestPromise(OFFICE_URLS.me_profile_url) 29 | .then((data: any) => { 30 | if (data) { 31 | resolve(); 32 | } else { 33 | reject(USER_MESSAGES.no_access_token); 34 | } 35 | }) 36 | .catch((err) => { 37 | reject(USER_MESSAGES.fail_graph_api + " " + err); 38 | }); 39 | }); 40 | 41 | return p; 42 | } 43 | 44 | public getRequestPromise = (reqUrl: string): Promise => { 45 | let p = new Promise((resolve: Function, reject: Function) => { 46 | let tokenPromise = this.tokenPromise(AZURE_CONFIG.endpoints.graphApiUri); 47 | 48 | tokenPromise.then((token: string) => { 49 | let headers = new Headers(); 50 | headers.append("Authorization", "Bearer " + token); 51 | 52 | this.http.get(AZURE_CONFIG.endpoints.graphApiUri + reqUrl, { headers: headers }) 53 | .map((res: any) => res.json()) 54 | .subscribe( 55 | (res: any) => resolve(res), 56 | (error: any) => { 57 | console.error(error); 58 | reject(error); 59 | }); 60 | }); 61 | }); 62 | 63 | return p; 64 | }; 65 | 66 | public getPhotoRequestPromise = (reqUrl: string): Promise => { 67 | let p = new Promise((resolve: Function, reject: Function) => { 68 | let tokenPromise = this.tokenPromise(AZURE_CONFIG.endpoints.graphApiUri); 69 | tokenPromise.then((token: string) => { 70 | let request = new XMLHttpRequest(); 71 | request.open("GET", AZURE_CONFIG.endpoints.graphApiUri + reqUrl); 72 | request.setRequestHeader("Authorization", "Bearer " + token); 73 | request.responseType = "blob"; 74 | request.onload = function () { 75 | if (request.readyState === 4 && request.status === 200) { 76 | let reader = new FileReader(); 77 | reader.onload = () => { 78 | resolve(reader.result); 79 | }; 80 | 81 | reader.readAsDataURL(request.response); 82 | } else { 83 | reject(USER_MESSAGES.fail_graph_api); 84 | } 85 | }; 86 | 87 | request.send(null); 88 | }); 89 | }); 90 | 91 | return p; 92 | }; 93 | 94 | private tokenPromise = (endpoint: string): Promise => { 95 | let p = new Promise((resolve: Function, reject: Function) => { 96 | let token = window.localStorage.getItem("access_token"); 97 | if (token && token !== "undefined") { 98 | resolve(token); 99 | } else { 100 | reject(USER_MESSAGES.no_access_token); 101 | } 102 | }); 103 | 104 | return p; 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /src/tasks/tasks.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-tasks", 10 | templateUrl: "src/tasks/view-tasks.html", 11 | }) 12 | export class TasksComponent implements OnInit { 13 | private tasks = []; 14 | 15 | constructor(private graph: GraphService, 16 | private toast: ToastComponent, 17 | private changeRef: ChangeDetectorRef) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.toast.show(USER_MESSAGES.get_tasks); 22 | this.getTasks(); 23 | } 24 | 25 | private getTasks() { 26 | this.graph.getRequestPromise(OFFICE_URLS.me_tasks_url) 27 | .then((data: any) => { 28 | if (data) { 29 | this.tasks = data.value; 30 | this.changeRef.detectChanges(); 31 | } else { 32 | this.toast.show(USER_MESSAGES.fail_graph_api); 33 | } 34 | }) 35 | .catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/tasks/view-tasks.html: -------------------------------------------------------------------------------- 1 |

My Tasks

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 23 | 24 | 25 |
TitleDue DateCreated By
15 | {{task.title}} 16 | 18 | {{task.dueDateTime}} 19 | 21 | {{task.createdBy}} 22 |
-------------------------------------------------------------------------------- /src/toast/toast.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injectable } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "my-toast", 5 | template: `
6 |
7 | 8 |
`, 9 | }) 10 | @Injectable() 11 | export class ToastComponent { 12 | 13 | public show(message: string) { 14 | let notify: any = document.querySelector(".mdl-js-snackbar"); 15 | notify.style.backgroundColor = "#FF5722"; 16 | notify.MaterialSnackbar.showSnackbar({ 17 | message: message, 18 | timeout: 4000, 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/trending/trending.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-trending", 10 | templateUrl: "src/trending/view-trending.html", 11 | }) 12 | export class TrendingComponent implements OnInit { 13 | private trends = []; 14 | 15 | constructor(private graph: GraphService, 16 | private toast: ToastComponent, 17 | private changeRef: ChangeDetectorRef) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.toast.show(USER_MESSAGES.get_trending); 22 | this.getTrending(); 23 | } 24 | 25 | private getTrending() { 26 | this.graph.getRequestPromise(OFFICE_URLS.me_trending_around_url) 27 | .then((data: any) => { 28 | if (data) { 29 | this.trends = data.value; 30 | this.changeRef.detectChanges(); 31 | } else { 32 | this.toast.show(USER_MESSAGES.fail_graph_api); 33 | } 34 | }) 35 | .catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/trending/view-trending.html: -------------------------------------------------------------------------------- 1 |

My Trending Files

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 23 | 24 | 25 |
NameURLLast Modified
15 | {{trend.name}} 16 | 18 | Link 19 | 21 | {{trend.DateTimeLastModified}} 22 |
-------------------------------------------------------------------------------- /src/users/users.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef } from "@angular/core"; 2 | 3 | import { GraphService } from "../services/graph.service"; 4 | import { ToastComponent } from "../toast/toast.component"; 5 | import { USER_MESSAGES } from "../messages/messages"; 6 | import { OFFICE_URLS } from "../office/office-urls"; 7 | 8 | @Component({ 9 | selector: "my-users", 10 | templateUrl: "src/users/view-users.html", 11 | }) 12 | export class UsersComponent implements OnInit { 13 | private users = []; 14 | 15 | constructor(private graph: GraphService, 16 | private toast: ToastComponent, 17 | private changeRef: ChangeDetectorRef) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.toast.show(USER_MESSAGES.get_users); 22 | this.getUsers(); 23 | } 24 | 25 | private getUsers() { 26 | this.graph.getRequestPromise(OFFICE_URLS.all_users_url) 27 | .then((data: any) => { 28 | if (data) { 29 | this.users = data.value; 30 | this.changeRef.detectChanges(); 31 | } else { 32 | this.toast.show(USER_MESSAGES.fail_graph_api) 33 | } 34 | }) 35 | .catch(() => { 36 | this.toast.show(USER_MESSAGES.fail_graph_api) 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/users/view-users.html: -------------------------------------------------------------------------------- 1 |

People

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 22 |
Display NameEmail
15 | {{user.displayName}} 16 | 18 | {{user.mail}} 19 |
-------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "removeComments": false, 9 | "noImplicitAny": false, 10 | "strictNullChecks": false 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "releases" 15 | ], 16 | "filesGlob": [ 17 | "./src/**/*.ts", 18 | "!./node_modules/**/*.ts" 19 | ], 20 | "awesomeTypescriptLoaderOptions": { 21 | "resolveGlobs": true, 22 | "forkChecker": true 23 | }, 24 | "compileOnSave": false, 25 | "buildOnSave": false, 26 | "atom": { "rewriteTsconfig": false } 27 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | * Possible values: 4 | * - the name of a built-in config 5 | * - the name of an NPM module which has a "main" file that exports a config object 6 | * - a relative path to a JSON file 7 | */ 8 | "extends": "tslint:recommended", 9 | "rules": { 10 | /* 11 | * Any rules specified here will override those from the base config we are extending. 12 | */ 13 | "curly": true 14 | }, 15 | "jsRules": { 16 | /* 17 | * Any rules specified here will override those from the base config we are extending. 18 | */ 19 | "curly": true 20 | }, 21 | "rulesDirectory": [ 22 | /* 23 | * A list of relative or absolute paths to directories that contain custom rules. 24 | * See the Custom Rules documentation below for more details. 25 | */ 26 | ] 27 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; 4 | var DashboardPlugin = require('webpack-dashboard/plugin'); 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | debug: true, 9 | 10 | entry: { 11 | '@angular': [ 12 | 'rxjs', 13 | 'reflect-metadata', 14 | 'zone.js' 15 | ], 16 | 'common': [], 17 | 'app': './src/app/main.ts' 18 | }, 19 | 20 | output: { 21 | path: __dirname + '/build/', 22 | publicPath: 'build/', 23 | filename: '[name].js', 24 | sourceMapFilename: '[name].js.map', 25 | chunkFilename: '[id].chunk.js' 26 | }, 27 | 28 | resolve: { 29 | extensions: ['','.ts','.js','.json', '.css', '.html'] 30 | }, 31 | 32 | module: { 33 | loaders: [ 34 | { 35 | test: /\.ts$/, 36 | loader: 'ts', 37 | exclude: [ /node_modules/, /releases/ ] 38 | }, 39 | { 40 | test: /\.json$/, 41 | loader: 'json' 42 | }, 43 | { 44 | test: /\.(css|html)$/, 45 | loader: 'raw' 46 | }, 47 | { 48 | test: /\.(png|jpg)$/, 49 | loader: 'url?limit=10000' 50 | } 51 | ] 52 | }, 53 | plugins: [ 54 | new CommonsChunkPlugin({ names: ['@angular', 'common'], minChunks: Infinity }), 55 | new webpack.ExternalsPlugin('commonjs', ['electron']), 56 | new DashboardPlugin() 57 | ], 58 | externals: [ 59 | { 60 | electron : "commonjs electron" 61 | } 62 | ], 63 | target:'electron-renderer' 64 | }; 65 | --------------------------------------------------------------------------------