├── .deployment ├── .gitignore ├── Build ├── Analyzer.ruleset └── stylecop.json ├── CODE_OF_CONDUCT.md ├── Deployment └── azuredeploy.json ├── LICENSE ├── Manifest ├── color.png ├── manifest.json └── outline.png ├── README.md ├── SECURITY.md ├── Source ├── Microsoft.Teams.Apps.DLLookup.sln └── Microsoft.Teams.Apps.DLLookup │ ├── Authentication │ └── AuthenticationServiceCollectionExtensions.cs │ ├── ClientApp │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── locales │ │ │ └── en-US │ │ │ └── translation.json │ ├── src │ │ ├── App.js │ │ ├── App.js.map │ │ ├── App.scss │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── apis │ │ │ ├── api-list.ts │ │ │ └── axios-jwt-decorator.ts │ │ ├── components │ │ │ ├── add-distribution-list │ │ │ │ ├── add-distribution-list.scss │ │ │ │ └── add-distribution-list.tsx │ │ │ ├── distribution-list-members │ │ │ │ ├── distribution-list-members.scss │ │ │ │ └── distribution-list-members.tsx │ │ │ ├── distribution-lists │ │ │ │ ├── distribution-lists.scss │ │ │ │ └── distribution-lists.tsx │ │ │ ├── error-page │ │ │ │ ├── error-page.scss │ │ │ │ └── error-page.tsx │ │ │ ├── group-chat-warning │ │ │ │ ├── group-chat-warning.scss │ │ │ │ └── group-chat-warning.tsx │ │ │ ├── pagination │ │ │ │ └── pagination.tsx │ │ │ └── sign-in-page │ │ │ │ ├── sign-in-page.scss │ │ │ │ ├── sign-in-page.tsx │ │ │ │ ├── sign-in-simple-end.tsx │ │ │ │ └── sign-in-simple-start.tsx │ │ ├── configVariables.ts │ │ ├── i18n.js │ │ ├── i18n.js.map │ │ ├── i18n.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── router │ │ │ ├── router.js │ │ │ ├── router.js.map │ │ │ └── router.tsx │ └── tsconfig.json │ ├── Constants │ └── PresenceStates.cs │ ├── Controllers │ ├── AuthenticationMetadataController.cs │ ├── BaseController.cs │ ├── DistributionListMembersController.cs │ ├── DistributionListsController.cs │ ├── PresenceController.cs │ └── UserPageSizeController.cs │ ├── Helpers │ ├── Extensions │ │ ├── ListExtensions.cs │ │ └── RepositoriesServiceCollectionExtensions.cs │ ├── GraphUtilityHelper.cs │ └── TokenAcquisitionHelper.cs │ ├── Microsoft.Teams.Apps.DLLookup.csproj │ ├── Models │ ├── AuthenticationInfo.cs │ ├── AzureAdOptions.cs │ ├── CacheOptions.cs │ ├── DistributionList.cs │ ├── DistributionListMember.cs │ ├── FavoriteDistributionListData.cs │ ├── FavoriteDistributionListMemberData.cs │ ├── FavoriteDistributionListMemberTableEntity.cs │ ├── FavoriteDistributionListTableEntity.cs │ ├── PeoplePresenceData.cs │ ├── StorageOptions.cs │ ├── UserPageSizeChoice.cs │ └── UserPageSizeChoiceTableEntity.cs │ ├── Program.cs │ ├── Repositories │ ├── BaseStorageProvider.cs │ ├── FavoriteDistributionListDataRepository.cs │ ├── FavoriteDistributionListMemberDataRepository.cs │ ├── FavoriteDistributionListMemberStorageProvider.cs │ ├── FavoriteDistributionListStorageProvider.cs │ ├── Interfaces │ │ ├── IFavoriteDistributionListDataRepository.cs │ │ ├── IFavoriteDistributionListMemberDataRepository.cs │ │ └── IPresenceDataRepository.cs │ ├── PresenceDataRepository.cs │ ├── UserPageSizeChoiceDataRepository.cs │ └── UserPageSizeStorageProvider.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── deploy.cmd └── global.json /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | deploy.cmd -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUNIT 47 | *.VisualState.xml 48 | TestResult.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- Backup*.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | /.vs 355 | /Source/.vs/Microsoft.Teams.Apps.DLLookup/v16 356 | /Source/Microsoft.Teams.Apps.DLLookup/obj 357 | /Source/Microsoft.Teams.Apps.DLLookup/Properties/PublishProfiles 358 | /Source/Microsoft.Teams.Apps.DLLookup/Properties 359 | /Source/Microsoft.Teams.Apps.DLLookup.Repositories/obj 360 | /Source/.vs/Microsoft.Teams.Apps.DLLookup 361 | /Source/Microsoft.Teams.Apps.DLLookup/bin/Debug -------------------------------------------------------------------------------- /Build/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Microsoft" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 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 | -------------------------------------------------------------------------------- /Manifest/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-contactgrouplookup/33c93fa41f30bdaab434f6d5252c4176e90f0cbf/Manifest/color.png -------------------------------------------------------------------------------- /Manifest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.5", 4 | "version": "1.0.0", 5 | "id": "4b8d2936-4e5f-4563-83ca-4bdad686c788", 6 | "packageName": "com.microsoft.teams.apptemplates.contactgrouplookup", 7 | "developer": { 8 | "name": "<>", 9 | "websiteUrl": "<>", 10 | "privacyUrl": "<>", 11 | "termsOfUseUrl": "<>" 12 | }, 13 | "icons": { 14 | "color": "color.png", 15 | "outline": "outline.png" 16 | }, 17 | "name": { 18 | "short": "Contact Group Lookup", 19 | "full": "Contact Group Lookup" 20 | }, 21 | "description": { 22 | "short": "The Contact Group Lookup app helps interact with members of a contact group", 23 | "full": "The Contact Group Lookup app helps users quickly browse through members of a contact group, see their current status, and start an individual or group chat with them. Frequently used contact groups can be pinned to the top so that they can be quickly accessed" 24 | }, 25 | "accentColor": "#FFFFFF", 26 | "staticTabs": [ 27 | { 28 | "entityId": "Home", 29 | "name": "Home", 30 | "contentUrl": "https://<>/dls", 31 | "scopes": [ 32 | "personal" 33 | ] 34 | } 35 | ], 36 | "permissions": [ 37 | "identity" 38 | ], 39 | "validDomains": [ 40 | "<>", 41 | "<>" 42 | ], 43 | "webApplicationInfo": { 44 | "id": "<>", 45 | "resource": "api://<>/<>" 46 | } 47 | } -------------------------------------------------------------------------------- /Manifest/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-contactgrouplookup/33c93fa41f30bdaab434f6d5252c4176e90f0cbf/Manifest/outline.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | products: 6 | - office-teams 7 | description: The Contact Group Lookup app helps interact with members of a contact group 8 | urlFragment: microsoft-teams-app-contactgrouplookup 9 | --- 10 | ### Note: This is a sample app for Microsoft Teams platform capabilities. The code is not actively managed by Microsoft. The complete source code is available, which allows you to explore it in detail or fork the code and alter it to meet your specific requirements. The source code in this repository may contain links and dependencies to other source code and libraries. Developers are advised to validate all dependencies and update and integrate the latest versions as appropriate. Deployment and support of apps based on this code will be the responsibility of your organization. 11 | # Contact Group Lookup App Template 12 | 13 | | [Documentation](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Home) | [Deployment guide](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Deployment-Guide) | [Architecture](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Solution-Overview) | 14 | | ---- | ---- | ---- | 15 | 16 | Contact Group (sometimes referred to as a distribution list) is very useful for organizations to manage communication with a group of individuals. Examples of contact groups include members of an emergency response team at a hospital, employees who work in a particular building, or employees who share a common hobby. 17 | The Contact Group Lookup app makes it easier to interact with members of a contact group directly from Microsoft Teams. Using the app, you can quickly view and chat with members, see their status on Teams, and even start a group chat with multiple members of the Contact Group. 18 | 19 | An example workflow in the app is described below: 20 | - A user starts by opening the Contact Group Lookup app and adds their preferred contact groups to the app. 21 | - They pin important contact groups to the top of the list by clicking the pin icon 22 | - The user then clicks the name of the contact group of interest 23 | - The user sorts the list by status and starts a group chat with members who are online. 24 | 25 | The following images show examples of the user interface of the app: 26 | 27 | ![Search contact groups](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Images/SearchContactGroups.png) 28 | 29 | ![Favorited contact groups](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Images/FavoritesScreen.png) 30 | 31 | ![Initiate Teams chat with group members](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Images/InitiateChat.png) 32 | 33 | 34 | ## Legal notice 35 | 36 | This app template is provided under the [MIT License](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/blob/master/LICENSE) terms. In addition to these terms, by using this app template you agree to the following: 37 | 38 | - You, not Microsoft, will license the use of your app to users or organization. 39 | 40 | - This app template is not intended to substitute your own regulatory due diligence or make you or your app compliant with respect to any applicable regulations, including but not limited to privacy, healthcare, employment, or financial regulations. 41 | 42 | - You are responsible for complying with all applicable privacy and security regulations including those related to use, collection and handling of any personal data by your app. This includes complying with all internal privacy and security policies of your organization if your app is developed to be sideloaded internally within your organization. Where applicable, you may be responsible for data related incidents or data subject requests for data collected through your app. 43 | 44 | - Any trademarks or registered trademarks of Microsoft in the United States and/or other countries and logos included in this repository are the property of Microsoft, and the license for this project does not grant you rights to use any Microsoft names, logos or trademarks outside of this repository. Microsoft’s general trademark guidelines can be found [here](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general.aspx). 45 | 46 | - If the app template enables access to any Microsoft Internet-based services (e.g., Office365), use of those services will be subject to the separately-provided terms of use. In such cases, Microsoft may collect telemetry data related to app template usage and operation. Use and handling of telemetry data will be performed in accordance with such terms of use. 47 | 48 | - Use of this template does not guarantee acceptance of your app to the Teams app store. To make this app available in the Teams app store, you will have to comply with the [submission and validation process](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/publish), and all associated requirements such as including your own privacy statement and terms of use for your app. 49 | 50 | ## Getting started 51 | 52 | Begin with the [Solution overview](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Solution-overview) to read about what the app does and how it works. 53 | 54 | When you're ready to try out Contact Group Lookup app, or to use it in your own organization, follow the steps in the [Deployment guide](https://github.com/OfficeDev/microsoft-teams-app-contactgrouplookup/wiki/Deployment-Guide). 55 | 56 | ### Known issue: 57 | The app is currently not supported on iOS devices. We are actively working on fixing the issue and will update the repo as soon as it is available. 58 | 59 | ## Contributing 60 | 61 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 62 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 63 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 64 | 65 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 66 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 67 | provided by the bot. You will only need to do this once across all repos using our CLA. 68 | 69 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 70 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 71 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 72 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29230.47 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Teams.Apps.DLLookup", "Microsoft.Teams.Apps.DLLookup\Microsoft.Teams.Apps.DLLookup.csproj", "{84F7A666-1FA7-4DFF-BD90-A7313FA78111}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {84F7A666-1FA7-4DFF-BD90-A7313FA78111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {84F7A666-1FA7-4DFF-BD90-A7313FA78111}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {84F7A666-1FA7-4DFF-BD90-A7313FA78111}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {84F7A666-1FA7-4DFF-BD90-A7313FA78111}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {313C3F2A-D09B-496C-A085-BB0F25D8D42B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Authentication/AuthenticationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Authentication 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Net.Http.Headers; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Authentication.AzureAD.UI; 13 | using Microsoft.AspNetCore.Authentication.JwtBearer; 14 | using Microsoft.Extensions.Configuration; 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.IdentityModel.Tokens; 17 | using Microsoft.Teams.Apps.DLLookup.Helpers; 18 | 19 | /// 20 | /// Extension class for registering authentication services in DI container. 21 | /// 22 | public static class AuthenticationServiceCollectionExtensions 23 | { 24 | private const string ClientIdConfigurationSettingsKey = "AzureAd:ClientId"; 25 | private const string TenantIdConfigurationSettingsKey = "AzureAd:TenantId"; 26 | private const string ApplicationIdURIConfigurationSettingsKey = "AzureAd:ApplicationIdURI"; 27 | private const string ValidIssuersConfigurationSettingsKey = "AzureAd:ValidIssuers"; 28 | private const string GraphScopeConfigurationSettingsKey = "AzureAd:GraphScope"; 29 | 30 | /// 31 | /// Extension method to register the authentication services. 32 | /// 33 | /// IServiceCollection instance. 34 | /// IConfiguration instance. 35 | public static void AddDLLookupAuthentication(this IServiceCollection services, IConfiguration configuration) 36 | { 37 | RegisterAuthenticationServices(services, configuration); 38 | } 39 | 40 | // This method works specifically for single tenant application. 41 | private static void RegisterAuthenticationServices( 42 | IServiceCollection services, 43 | IConfiguration configuration) 44 | { 45 | AuthenticationServiceCollectionExtensions.ValidateAuthenticationConfigurationSettings(configuration); 46 | 47 | services.AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) 48 | .AddJwtBearer(options => 49 | { 50 | var azureADOptions = new AzureADOptions(); 51 | configuration.Bind("AzureAd", azureADOptions); 52 | options.Authority = $"{azureADOptions.Instance}{azureADOptions.TenantId}/v2.0"; 53 | options.TokenValidationParameters = new TokenValidationParameters 54 | { 55 | ValidAudiences = AuthenticationServiceCollectionExtensions.GetValidAudiences(configuration), 56 | ValidIssuers = AuthenticationServiceCollectionExtensions.GetValidIssuers(configuration), 57 | AudienceValidator = AuthenticationServiceCollectionExtensions.AudienceValidator, 58 | }; 59 | options.Events = new JwtBearerEvents 60 | { 61 | OnTokenValidated = async context => 62 | { 63 | var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService(); 64 | context.Success(); 65 | 66 | // Adds the token to the cache, and also handles the incremental consent and claim challenges 67 | var jwtToken = AuthenticationHeaderValue.Parse(context.Request.Headers["Authorization"].ToString()).Parameter; 68 | await tokenAcquisition.AddTokenToCacheFromJwtAsync(configuration[AuthenticationServiceCollectionExtensions.GraphScopeConfigurationSettingsKey], jwtToken); 69 | await Task.FromResult(0); 70 | }, 71 | }; 72 | }); 73 | } 74 | 75 | private static void ValidateAuthenticationConfigurationSettings(IConfiguration configuration) 76 | { 77 | var clientId = configuration[AuthenticationServiceCollectionExtensions.ClientIdConfigurationSettingsKey]; 78 | if (string.IsNullOrWhiteSpace(clientId)) 79 | { 80 | throw new ApplicationException("AzureAD ClientId is missing in the configuration file."); 81 | } 82 | 83 | var tenantId = configuration[AuthenticationServiceCollectionExtensions.TenantIdConfigurationSettingsKey]; 84 | if (string.IsNullOrWhiteSpace(tenantId)) 85 | { 86 | throw new ApplicationException("AzureAD TenantId is missing in the configuration file."); 87 | } 88 | 89 | var applicationIdURI = configuration[AuthenticationServiceCollectionExtensions.ApplicationIdURIConfigurationSettingsKey]; 90 | if (string.IsNullOrWhiteSpace(applicationIdURI)) 91 | { 92 | throw new ApplicationException("AzureAD ApplicationIdURI is missing in the configuration file."); 93 | } 94 | 95 | var validIssuers = configuration[AuthenticationServiceCollectionExtensions.ValidIssuersConfigurationSettingsKey]; 96 | if (string.IsNullOrWhiteSpace(validIssuers)) 97 | { 98 | throw new ApplicationException("AzureAD ValidIssuers is missing in the configuration file."); 99 | } 100 | } 101 | 102 | private static IEnumerable GetSettings(IConfiguration configuration, string configurationSettingsKey) 103 | { 104 | var configurationSettingsValue = configuration[configurationSettingsKey]; 105 | var settings = configurationSettingsValue 106 | ?.Split(new char[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries) 107 | ?.Select(p => p.Trim()); 108 | if (settings == null) 109 | { 110 | throw new ApplicationException($"{configurationSettingsKey} does not contain a valid value in the configuration file."); 111 | } 112 | 113 | return settings; 114 | } 115 | 116 | private static IEnumerable GetValidAudiences(IConfiguration configuration) 117 | { 118 | var clientId = configuration[AuthenticationServiceCollectionExtensions.ClientIdConfigurationSettingsKey]; 119 | 120 | var applicationIdURI = configuration[AuthenticationServiceCollectionExtensions.ApplicationIdURIConfigurationSettingsKey]; 121 | 122 | var validAudiences = new List { clientId, applicationIdURI.ToUpperInvariant() }; 123 | 124 | return validAudiences; 125 | } 126 | 127 | private static IEnumerable GetValidIssuers(IConfiguration configuration) 128 | { 129 | var tenantId = configuration[AuthenticationServiceCollectionExtensions.TenantIdConfigurationSettingsKey]; 130 | 131 | var validIssuers = 132 | AuthenticationServiceCollectionExtensions.GetSettings( 133 | configuration, 134 | AuthenticationServiceCollectionExtensions.ValidIssuersConfigurationSettingsKey); 135 | 136 | validIssuers = validIssuers.Select(validIssuer => validIssuer.Replace("TENANT_ID", tenantId, StringComparison.OrdinalIgnoreCase)); 137 | 138 | return validIssuers; 139 | } 140 | 141 | private static bool AudienceValidator( 142 | IEnumerable tokenAudiences, 143 | SecurityToken securityToken, 144 | TokenValidationParameters validationParameters) 145 | { 146 | if (tokenAudiences == null || !tokenAudiences.Any()) 147 | { 148 | throw new ApplicationException("No audience defined in token!"); 149 | } 150 | 151 | var validAudiences = validationParameters.ValidAudiences; 152 | if (validAudiences == null || !validAudiences.Any()) 153 | { 154 | throw new ApplicationException("No valid audiences defined in validationParameters!"); 155 | } 156 | 157 | foreach (var tokenAudience in tokenAudiences) 158 | { 159 | if (validAudiences.Any(validAudience => validAudience.Equals(tokenAudience, StringComparison.OrdinalIgnoreCase))) 160 | { 161 | return true; 162 | } 163 | } 164 | 165 | return false; 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dllookup-client-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "1.2.28", 7 | "@fortawesome/free-solid-svg-icons": "5.13.0", 8 | "@fortawesome/react-fontawesome": "0.1.10", 9 | "@microsoft/teams-js": "1.6.0", 10 | "@stardust-ui/react": "0.40.7", 11 | "axios": "0.19.2", 12 | "browserslist": "4.12.0", 13 | "caniuse-lite": "1.0.30001079", 14 | "font-awesome": "4.7.0", 15 | "i18next": "19.4.5", 16 | "i18next-xhr-backend": "3.2.2", 17 | "lodash": "4.17.15", 18 | "msteams-ui-components-react": "0.8.4", 19 | "node-sass": "4.14.1", 20 | "office-ui-fabric-react": "7.118.0", 21 | "react": "16.13.1", 22 | "react-dom": "16.13.1", 23 | "react-i18next": "11.5.0", 24 | "react-redux": "7.2.0", 25 | "react-router-dom": "5.2.0", 26 | "react-scripts": "3.4.1", 27 | "redux": "4.0.5", 28 | "redux-thunk": "2.3.0", 29 | "typescript": "3.9.5", 30 | "typestyle": "2.1.0" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "@fortawesome/fontawesome-free": "5.13.0", 55 | "@types/react-redux": "7.1.9", 56 | "@types/jest": "25.2.3", 57 | "@types/lodash": "4.14.155", 58 | "@types/node": "14.0.12", 59 | "@types/react": "16.9.35", 60 | "@types/react-dom": "16.9.8", 61 | "@types/react-router-dom": "5.1.5", 62 | "@types/redux": "3.6.31" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 25 | DL Lookup 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/public/locales/en-US/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "distributionListsTitle": "Contact groups", 3 | "distributionList": "Contact group", 4 | "viewDetails": "View details", 5 | "pageSizeGroups": "Groups per page", 6 | "pageSizeMembers": "Members per page", 7 | "search": "Search", 8 | "addDistributionList": "Add contact group", 9 | "appInfo": "Click on a contact group to see which members are online and message them directly.", 10 | "headerName": "Contact group", 11 | "headerAlias": "Email", 12 | "headerMembersCount": "Member count", 13 | "headerMembersOnline": "Members online", 14 | "headerActions": "Actions", 15 | "headerContactName": "Member name", 16 | "headerContactAlias": "Email", 17 | "headerPresenceStatus": "Status in Teams", 18 | "presenceOnline": "Available", 19 | "presenceOffline": "Offline", 20 | "presenceBeRightBack": "Be right back", 21 | "presenceNone": "None", 22 | "presenceAway": "Away", 23 | "presenceDoNotDisturb": "Do not disturb", 24 | "presenceBusy": "Busy", 25 | "startGroupChat": "Start chat", 26 | "goBack": "Go back", 27 | "addFavoriteDistributionList": "Add a contact group", 28 | "searchByDlName": "Search existing contact groups", 29 | "add": "Add", 30 | "close": "Close", 31 | "pin": "Pin on top", 32 | "unpin": "Unpin", 33 | "delete": "Remove", 34 | "welcomeMessage": "Welcome to Contact Group Lookup", 35 | "getStarted": "Start by adding one or more Outlook contact groups. From there, you can see which members are online and message them in Teams.", 36 | "noSearchResults": "No results found. Try searching again.", 37 | "unauthorizedErrorMessage": "Something went wrong. Try again in a few minutes.", 38 | "forbiddenErrorMessage": "Permissions needed. Contact your IT admin to request access.", 39 | "generalErrorMessage": "Something went wrong. Try again in a few minutes. If the problem persists, contact your IT admin.", 40 | "groupChatMessage": "Can we start a smaller group chat?", 41 | "groupChatCountMessage": "You selected more than 100 members to chat with. Currently, group chats in Teams are limited to 100 people.", 42 | "groupChatRecentMembers": "Would you like us to start a chat now with the 100 members who are currently online?", 43 | "yes": "Start chat", 44 | "no": "Cancel", 45 | "removeDl": "Remove contact group", 46 | "signInMessage": "Please sign in to continue.", 47 | "signIn": "Sign in" 48 | } 49 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/App.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // 3 | // Copyright (c) Microsoft. All rights reserved. 4 | // 5 | var __extends = (this && this.__extends) || (function () { 6 | var extendStatics = function (d, b) { 7 | extendStatics = Object.setPrototypeOf || 8 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 9 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 10 | return extendStatics(d, b); 11 | }; 12 | return function (d, b) { 13 | extendStatics(d, b); 14 | function __() { this.constructor = d; } 15 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 16 | }; 17 | })(); 18 | Object.defineProperty(exports, "__esModule", { value: true }); 19 | var React = require("react"); 20 | require("./App.scss"); 21 | var react_1 = require("@stardust-ui/react"); 22 | var microsoftTeams = require("@microsoft/teams-js"); 23 | var router_1 = require("./router/router"); 24 | var App = /** @class */ (function (_super) { 25 | __extends(App, _super); 26 | function App(props) { 27 | var _this = _super.call(this, props) || this; 28 | _this.componentDidMount = function () { 29 | microsoftTeams.initialize(); 30 | microsoftTeams.getContext(function (context) { 31 | var theme = context.theme || ""; 32 | _this.setState({ 33 | theme: theme 34 | }); 35 | }); 36 | microsoftTeams.registerOnThemeChangeHandler(function (theme) { 37 | _this.setState({ 38 | theme: theme, 39 | }, function () { 40 | _this.forceUpdate(); 41 | }); 42 | }); 43 | }; 44 | _this.setThemeComponent = function () { 45 | if (_this.state.theme === "dark") { 46 | return (React.createElement(react_1.Provider, { theme: react_1.themes.teamsDark }, 47 | React.createElement("div", { className: "darkContainer" }, _this.getAppDom()))); 48 | } 49 | else if (_this.state.theme === "contrast") { 50 | return (React.createElement(react_1.Provider, { theme: react_1.themes.teamsHighContrast }, 51 | React.createElement("div", { className: "highContrastContainer" }, _this.getAppDom()))); 52 | } 53 | else { 54 | return (React.createElement(react_1.Provider, { theme: react_1.themes.teams }, 55 | React.createElement("div", { className: "default-container" }, _this.getAppDom()))); 56 | } 57 | }; 58 | _this.getAppDom = function () { 59 | return (React.createElement("div", { className: "app-container" }, 60 | React.createElement(router_1.AppRoute, null))); 61 | }; 62 | _this.state = { 63 | theme: "", 64 | }; 65 | return _this; 66 | } 67 | App.prototype.render = function () { 68 | return (React.createElement("div", null, this.setThemeComponent())); 69 | }; 70 | return App; 71 | }(React.Component)); 72 | exports.default = App; 73 | //# sourceMappingURL=App.js.map -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/App.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"App.js","sourceRoot":"","sources":["App.tsx"],"names":[],"mappings":";AAAA,iDAAiD;AACjD,gDAAgD;AAChD,eAAe;;;;;;;;;;;;;;;AAEf,6BAA+B;AAC/B,sBAAoB;AACpB,4CAAsD;AACtD,oDAAsD;AACtD,0CAA2C;AAM3C;IAAkB,uBAA8B;IAE5C,aAAY,KAAS;QAArB,YACI,kBAAM,KAAK,CAAC,SAIf;QAEM,uBAAiB,GAAG;YACvB,cAAc,CAAC,UAAU,EAAE,CAAC;YAC5B,cAAc,CAAC,UAAU,CAAC,UAAC,OAAO;gBAC9B,IAAI,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;gBAChC,KAAI,CAAC,QAAQ,CAAC;oBACV,KAAK,EAAE,KAAK;iBACf,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;YAEH,cAAc,CAAC,4BAA4B,CAAC,UAAC,KAAK;gBAC9C,KAAI,CAAC,QAAQ,CAAC;oBACV,KAAK,EAAE,KAAK;iBACf,EAAE;oBACC,KAAI,CAAC,WAAW,EAAE,CAAC;gBACvB,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;QACP,CAAC,CAAA;QAEM,uBAAiB,GAAG;YACvB,IAAI,KAAI,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM,EAAE;gBAC7B,OAAO,CACH,oBAAC,gBAAQ,IAAC,KAAK,EAAE,cAAM,CAAC,SAAS;oBAC7B,6BAAK,SAAS,EAAC,eAAe,IACzB,KAAI,CAAC,SAAS,EAAE,CACf,CACC,CACd,CAAC;aACL;iBACI,IAAI,KAAI,CAAC,KAAK,CAAC,KAAK,KAAK,UAAU,EAAE;gBACtC,OAAO,CACH,oBAAC,gBAAQ,IAAC,KAAK,EAAE,cAAM,CAAC,iBAAiB;oBACrC,6BAAK,SAAS,EAAC,uBAAuB,IACjC,KAAI,CAAC,SAAS,EAAE,CACf,CACC,CACd,CAAC;aACL;iBAAM;gBACH,OAAO,CACH,oBAAC,gBAAQ,IAAC,KAAK,EAAE,cAAM,CAAC,KAAK;oBACzB,6BAAK,SAAS,EAAC,mBAAmB,IAC7B,KAAI,CAAC,SAAS,EAAE,CACf,CACC,CACd,CAAC;aACL;QACL,CAAC,CAAA;QAEM,eAAS,GAAG;YACf,OAAO,CAEC,6BAAK,SAAS,EAAC,eAAe;gBACzB,oBAAC,iBAAQ,OAAG,CACX,CACb,CAAC;QACN,CAAC,CAAA;QA3DG,KAAI,CAAC,KAAK,GAAG;YACT,KAAK,EAAE,EAAE;SACZ,CAAA;;IACL,CAAC;IA0DM,oBAAM,GAAb;QACI,OAAO,CACH,iCACK,IAAI,CAAC,iBAAiB,EAAE,CACvB,CACT,CAAC;IACN,CAAC;IACL,UAAC;AAAD,CAAC,AAxED,CAAkB,KAAK,CAAC,SAAS,GAwEhC;AAED,kBAAe,GAAG,CAAC"} -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import App from './App'; 8 | 9 | it('renders without crashing', () => { 10 | const div = document.createElement('div'); 11 | ReactDOM.render(, div); 12 | ReactDOM.unmountComponentAtNode(div); 13 | }); 14 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/App.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from 'react'; 6 | import './App.scss'; 7 | import { Provider, themes } from '@stardust-ui/react'; 8 | import * as microsoftTeams from "@microsoft/teams-js"; 9 | import { AppRoute } from "./router/router"; 10 | 11 | export interface IAppState { 12 | theme: string; 13 | } 14 | 15 | class App extends React.Component<{}, IAppState> { 16 | 17 | constructor(props: {}) { 18 | super(props); 19 | this.state = { 20 | theme: "", 21 | } 22 | } 23 | 24 | public componentDidMount = () => { 25 | microsoftTeams.initialize(); 26 | microsoftTeams.getContext((context) => { 27 | let theme = context.theme || ""; 28 | this.setState({ 29 | theme: theme 30 | }); 31 | }); 32 | 33 | microsoftTeams.registerOnThemeChangeHandler((theme) => { 34 | this.setState({ 35 | theme: theme, 36 | }, () => { 37 | this.forceUpdate(); 38 | }); 39 | }); 40 | } 41 | 42 | public setThemeComponent = () => { 43 | if (this.state.theme === "dark") { 44 | return ( 45 | 46 |
47 | {this.getAppDom()} 48 |
49 |
50 | ); 51 | } 52 | else if (this.state.theme === "contrast") { 53 | return ( 54 | 55 |
56 | {this.getAppDom()} 57 |
58 |
59 | ); 60 | } else { 61 | return ( 62 | 63 |
64 | {this.getAppDom()} 65 |
66 |
67 | ); 68 | } 69 | } 70 | 71 | public getAppDom = () => { 72 | return ( 73 | 74 |
75 | 76 |
77 | ); 78 | } 79 | 80 | public render(): JSX.Element { 81 | return ( 82 |
83 | {this.setThemeComponent()} 84 |
85 | ); 86 | } 87 | } 88 | 89 | export default App; -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/apis/api-list.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import axios from './axios-jwt-decorator'; 6 | import { getBaseUrl } from '../configVariables'; 7 | import { AxiosResponse } from "axios"; 8 | import { IADDistributionList } from "./../components/add-distribution-list/add-distribution-list" 9 | import { IDistributionListMember, IUserPageSizeChoice, IPresenceData } from "./../components/distribution-list-members/distribution-list-members" 10 | import { IDistributionList } from "./../components/distribution-lists/distribution-lists" 11 | 12 | let baseAxiosUrl = getBaseUrl() + '/api'; 13 | 14 | export const getFavoriteDistributionLists = async (): Promise> => { 15 | let url = baseAxiosUrl + "/distributionlists"; 16 | return await axios.get(url); 17 | } 18 | 19 | export const getADDistributionLists = async (query: string): Promise> => { 20 | let url = baseAxiosUrl + "/distributionlists/getDistributionList?query=" + encodeURIComponent(query); 21 | return await axios.get(url); 22 | } 23 | 24 | export const createFavoriteDistributionList = async (payload: {}): Promise> => { 25 | let url = baseAxiosUrl + "/distributionlists"; 26 | return await axios.post(url, payload); 27 | } 28 | 29 | export const updateFavoriteDistributionList = async (payload: {}): Promise> => { 30 | let url = baseAxiosUrl + "/distributionlists"; 31 | return await axios.put(url, payload); 32 | } 33 | 34 | export const deleteFavoriteDistributionList = async (payload: {}): Promise> => { 35 | let url = baseAxiosUrl + "/distributionlists"; 36 | return await axios.delete(url, payload); 37 | } 38 | 39 | export const getDistributionListsMembers = async (groupId?: string): Promise> => { 40 | let url = baseAxiosUrl + "/distributionlistmembers?groupId=" + groupId; 41 | return await axios.get(url); 42 | } 43 | 44 | export const pinStatusUpdate = async (pinnedUser: string, status: boolean, distributionListId: string): Promise> => { 45 | var payload = { 46 | "pinnedUserId": pinnedUser, 47 | "distributionListId": distributionListId 48 | } 49 | if (status) { 50 | let url = baseAxiosUrl + "/distributionlistmembers"; 51 | return await axios.post(url, payload); 52 | } 53 | else { 54 | let url = baseAxiosUrl + "/distributionlistmembers"; 55 | return await axios.delete(url, payload); 56 | } 57 | } 58 | 59 | export const getDistributionListMembersOnlineCount = async (groupId?: string): Promise> => { 60 | let url = baseAxiosUrl + "/presence/GetDistributionListMembersOnlineCount?groupId=" + groupId; 61 | return await axios.get(url); 62 | } 63 | 64 | export const getUserPresence = async (payload: {}): Promise> => { 65 | let url = baseAxiosUrl + "/presence/getUserPresence"; 66 | return await axios.post(url, payload); 67 | } 68 | 69 | export const getUserPageSizeChoice = async (): Promise> => { 70 | let url = baseAxiosUrl + "/UserPageSize"; 71 | return await axios.get(url); 72 | } 73 | 74 | export const createUserPageSizeChoice = async (payload: {}): Promise> => { 75 | let url = baseAxiosUrl + "/UserPageSize"; 76 | return await axios.post(url, payload); 77 | } 78 | 79 | export const getAuthenticationMetadata = async (windowLocationOriginDomain: string, loginHint: string): Promise> => { 80 | const payload = { windowLocationOriginDomain: windowLocationOriginDomain, loginhint: loginHint }; 81 | let url = `${baseAxiosUrl}/authenticationMetadata/GetAuthenticationUrlWithConfiguration`; 82 | return await axios.post(url, payload); 83 | } 84 | 85 | export const getClientId = async (): Promise> => { 86 | let url = baseAxiosUrl + "/authenticationMetadata/getClientId"; 87 | return await axios.get(url); 88 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/apis/axios-jwt-decorator.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import axios, { AxiosResponse, AxiosRequestConfig } from "axios"; 6 | import * as microsoftTeams from "@microsoft/teams-js"; 7 | 8 | export class AxiosJWTDecorator { 9 | public async get>( 10 | url: string, 11 | config?: AxiosRequestConfig, 12 | needAuthorizationHeader: boolean = true, 13 | ): Promise { 14 | try { 15 | if (needAuthorizationHeader) { 16 | config = await this.setupAuthorizationHeader(config); 17 | } 18 | return await axios.get(url, config); 19 | } catch (error) { 20 | this.handleError(error); 21 | throw error; 22 | } 23 | } 24 | 25 | public async delete>( 26 | url: string, 27 | data?: any, 28 | config?: AxiosRequestConfig 29 | ): Promise { 30 | try { 31 | config = await this.setupAuthorizationHeader(config); 32 | if (data) { 33 | config.headers["Content-Type"] = 'application/json; charset=utf-8'; 34 | config.data = data; 35 | } 36 | return await axios.delete(url, config); 37 | } catch (error) { 38 | this.handleError(error); 39 | throw error; 40 | } 41 | } 42 | 43 | public async post>( 44 | url: string, 45 | data?: any, 46 | config?: AxiosRequestConfig 47 | ): Promise { 48 | try { 49 | config = await this.setupAuthorizationHeader(config); 50 | return await axios.post(url, data, config); 51 | } catch (error) { 52 | this.handleError(error); 53 | throw error; 54 | } 55 | } 56 | 57 | public async put>( 58 | url: string, 59 | data?: any, 60 | config?: AxiosRequestConfig 61 | ): Promise { 62 | try { 63 | config = await this.setupAuthorizationHeader(config); 64 | return await axios.put(url, data, config); 65 | } catch (error) { 66 | this.handleError(error); 67 | throw error; 68 | } 69 | } 70 | 71 | private handleError(error: any): void { 72 | if (error.hasOwnProperty("response")) { 73 | console.log(error); 74 | const errorStatus = error.response.status; 75 | if (errorStatus === 403) { 76 | window.location.href = "/errorpage/403"; 77 | } else if (errorStatus === 401) { 78 | window.location.href = "/errorpage/401"; 79 | } else { 80 | window.location.href = "/errorpage"; 81 | } 82 | } else { 83 | window.location.href = "/errorpage"; 84 | } 85 | } 86 | 87 | private async setupAuthorizationHeader( 88 | config?: AxiosRequestConfig 89 | ): Promise { 90 | microsoftTeams.initialize(); 91 | 92 | return new Promise((resolve, reject) => { 93 | const authTokenRequest = { 94 | successCallback: (token: string) => { 95 | if (!config) { 96 | config = axios.defaults; 97 | } 98 | config.headers["Authorization"] = `Bearer ${token}`; 99 | resolve(config); 100 | }, 101 | failureCallback: (error: string) => { 102 | // When the getAuthToken function returns a "resourceRequiresConsent" error, 103 | // it means Azure AD needs the user's consent before issuing a token to the app. 104 | // The following code redirects the user to the "Sign in" page where the user can grant the consent. 105 | // Right now, the app redirects to the consent page for any error. 106 | console.error("Error from getAuthToken: ", error); 107 | window.location.href = "/signin"; 108 | }, 109 | resources: ["https://graph.microsoft.com"] 110 | }; 111 | microsoftTeams.authentication.getAuthToken(authTokenRequest); 112 | }); 113 | } 114 | } 115 | 116 | const axiosJWTDecoratorInstance = new AxiosJWTDecorator(); 117 | export default axiosJWTDecoratorInstance; -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/add-distribution-list/add-distribution-list.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | .task-module { 5 | .form-container { 6 | width: 100%; 7 | padding-right: 0.8rem; 8 | padding-left: 0.8rem; 9 | margin-top: 0.8rem; 10 | 11 | .form-content-container { 12 | margin-top: 0.8rem; 13 | overflow-y: auto !important; 14 | position: absolute; 15 | height: 390px; 16 | width: 92%; 17 | margin-left: 2%; 18 | margin-right: 2%; 19 | } 20 | } 21 | 22 | .highlight { 23 | font-weight: bold; 24 | } 25 | 26 | .footer-container { 27 | position: absolute; 28 | bottom: 0; 29 | left: 0; 30 | padding-right: 0.8rem; 31 | padding-left: 0.8rem; 32 | width: 100%; 33 | margin-bottom: 0.8rem; 34 | 35 | .button-container { 36 | display: inline; 37 | float: right; 38 | margin-right: 16px; 39 | } 40 | } 41 | 42 | .search-not-found { 43 | text-align: center; 44 | margin-top: 2rem; 45 | font-size: 1.5em; 46 | } 47 | } 48 | 49 | .welcome-text { 50 | color: #6264A7; 51 | font-size: 18px; 52 | font-weight: bold; 53 | } 54 | 55 | .get-started { 56 | color: #6264A7; 57 | font-size: 14px; 58 | } 59 | 60 | .task-module-border { 61 | border-bottom-color: #FFFFFF !important; 62 | } 63 | 64 | .app-container { 65 | background: #FFFFFF !important; 66 | } 67 | 68 | .search-div { 69 | margin-left: 2% !important; 70 | margin-right: 2% !important; 71 | } 72 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/distribution-list-members/distribution-list-members.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | .main-component { 6 | height: 100% !important; 7 | overflow-y: hidden; 8 | 9 | 10 | .form-container { 11 | width: 100%; 12 | padding-right: 0.8rem; 13 | padding-left: 0.8rem; 14 | overflow-y: hidden; 15 | height: 87% !important; 16 | 17 | .form-content-container { 18 | margin-bottom: 0.5rem; 19 | margin-top: 0.1rem !important; 20 | overflow-y: auto !important; 21 | height: 87% !important; 22 | width: 100%; 23 | } 24 | } 25 | 26 | .footer-container { 27 | position: relative !important; 28 | bottom: 0; 29 | left: 0; 30 | padding: 0 !important; 31 | width: 100%; 32 | } 33 | } 34 | 35 | .paging-segment { 36 | padding: 0 !important; 37 | } 38 | .maindiv 39 | { 40 | text-align:center; 41 | padding-top: 200px; 42 | padding-bottom: 200px; 43 | height:100% 44 | } 45 | .contentdiv { 46 | padding-top:5px; 47 | height: 100vh !important; 48 | } 49 | .textstyle { 50 | padding-left: 5px; 51 | padding-right: 5px; 52 | font-size: 1.2rem !important; 53 | } 54 | .textstyle-back { 55 | padding-left: 5px; 56 | padding-right: 5px; 57 | font-size: 0.8rem !important; 58 | } 59 | .emptydiv { 60 | text-align: center; 61 | padding-top: 200px; 62 | padding-bottom: 200px; 63 | } 64 | .action-section{ 65 | .seperator-spacing { 66 | margin-left: 0.625rem; 67 | } 68 | } 69 | #action-section Anchor { 70 | vertical-align: middle; 71 | text-align: center; 72 | } 73 | .div-style { 74 | width: 180px !important; 75 | } 76 | 77 | .title-sort-icon:hover { 78 | cursor: pointer; 79 | } 80 | 81 | .group-checkbox{ 82 | margin-right: 0px !important; 83 | padding-right: 0px !important; 84 | } 85 | 86 | .margin{ 87 | margin-left: 50% !important; 88 | } 89 | 90 | .nav-header { 91 | font-size: 18px; 92 | color: #252423 !important; 93 | } 94 | .nav-header:hover { 95 | cursor: pointer; 96 | text-decoration: underline; 97 | color: #252423 !important; 98 | } 99 | .nav-header-text { 100 | font-size: 18px; 101 | font-weight: 600; 102 | color: #252423 !important; 103 | } 104 | .nav-header-arrow { 105 | font-size: 18px; 106 | padding-left: 5px !important; 107 | padding-right: 5px !important; 108 | color: #252423 !important; 109 | } 110 | border-none { 111 | border: none !important; 112 | box-shadow: none !important; 113 | border-bottom: 1px solid #d9d9d9 !important; 114 | background-color: #F9F8F7 !important; 115 | color: #252423 !important; 116 | } 117 | border-none:hover { 118 | color: #252423 !important; 119 | } 120 | 121 | .dark-theme { 122 | color: #252423 !important; 123 | } 124 | 125 | .dark-theme:hover { 126 | color: #252423 !important; 127 | } 128 | .presence-icon{ 129 | margin-top:4px; 130 | } 131 | .margin-style{ 132 | margin-right: 0.3rem !important; 133 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/distribution-lists/distribution-lists.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | .main-component { 6 | height: 100% !important; 7 | overflow-y: hidden; 8 | 9 | .form-container { 10 | width: 100%; 11 | padding-right: 0.8rem; 12 | padding-left: 0.8rem; 13 | margin-top: 0.8rem; 14 | overflow-y: hidden; 15 | height: 87% !important; 16 | 17 | .form-content-container { 18 | margin-top: 0.8rem; 19 | margin-bottom: 0.8rem; 20 | overflow-y: auto !important; 21 | height: 87% !important; 22 | width: 98%; 23 | } 24 | } 25 | 26 | .footer-container { 27 | position: relative !important; 28 | bottom: 0; 29 | left: 0; 30 | padding: 0 !important; 31 | width: 100%; 32 | } 33 | 34 | .footer-container > div { 35 | background: #F3F2F1; 36 | } 37 | } 38 | 39 | .ui-dropdown__container { 40 | width: 10rem; 41 | } 42 | 43 | .paging-segment { 44 | padding: 0 !important; 45 | } 46 | 47 | .emptydiv { 48 | text-align: center; 49 | padding-top: 12%; 50 | padding-bottom: 12%; 51 | padding-left: 35%; 52 | padding-right: 35%; 53 | } 54 | 55 | .textstyle { 56 | padding-left: 5px; 57 | padding-right: 5px; 58 | padding-top: 5px; 59 | font-size: 14px !important; 60 | color: #252423 !important; 61 | } 62 | 63 | .search-box { 64 | padding-left: 5px; 65 | padding-right: 5px; 66 | font-size: 14px !important; 67 | color: #484644 !important; 68 | } 69 | 70 | .div-style { 71 | width: 165px !important; 72 | } 73 | 74 | .bg-color > div { 75 | background: #F9F8F7 !important; 76 | } 77 | 78 | .bg-color button { 79 | color: #484644 !important; 80 | } 81 | 82 | .bg-color div:hover { 83 | cursor: pointer; 84 | } 85 | 86 | .seperator-spacing { 87 | margin-left: 0.625rem; 88 | } 89 | 90 | .seperator-spacing:hover { 91 | cursor: pointer 92 | } 93 | 94 | .title:hover { 95 | cursor: pointer; 96 | text-decoration: underline; 97 | } 98 | 99 | .border-none { 100 | border: none !important; 101 | box-shadow: none !important; 102 | border-bottom: 1px solid #d9d9d9 !important; 103 | background-color: #F9F8F7 !important; 104 | color: #252423 !important; 105 | } 106 | 107 | .border-none:hover { 108 | color: #252423 !important; 109 | } 110 | 111 | .header { 112 | border: none !important; 113 | box-shadow: none !important; 114 | border-bottom: 1px solid #d9d9d9 !important; 115 | font-size: 0.75rem !important; 116 | background-color: #F9F8F7 !important; 117 | } 118 | 119 | .title-sort-icon { 120 | padding-top: 0px; 121 | color: #252423 !important; 122 | } 123 | 124 | .title-sort-icon:hover { 125 | cursor: pointer; 126 | } 127 | 128 | .disable-pin { 129 | color: #c8c6c4; 130 | } 131 | 132 | .main-component { 133 | background: #F3F2F1 !important; 134 | } 135 | 136 | .app-container { 137 | background: #F3F2F1 !important; 138 | } 139 | 140 | .actions-style { 141 | padding-left: 4.5em !important; 142 | } 143 | 144 | .margin-style { 145 | margin-right: 0.3rem !important; 146 | } 147 | 148 | .info-icon { 149 | padding-left: 1%; 150 | font-size: small; 151 | } 152 | 153 | .default-container { 154 | 155 | .search-box input { 156 | background-color: #FFFFFF !important; 157 | border-radius: 3px; 158 | color: #484644 !important; 159 | } 160 | 161 | .search-box input:-ms-input-placeholder { 162 | color: #484644 !important; 163 | } 164 | 165 | .text-style { 166 | color: #252423 !important; 167 | } 168 | } 169 | 170 | .darkContainer { 171 | 172 | .main-component { 173 | background: #292929 !important; 174 | 175 | .form-content-container { 176 | .textstyle { 177 | color: rgb(200, 198, 196) !important; 178 | } 179 | } 180 | 181 | .textstyle { 182 | color: rgb(200, 198, 196) !important; 183 | } 184 | } 185 | 186 | .header { 187 | border-bottom: 1px solid #292929 !important; 188 | background-color: rgb(32, 31, 31) !important; 189 | 190 | .dark-theme { 191 | color: rgb(200, 198, 196) !important; 192 | } 193 | } 194 | 195 | .border-none { 196 | border-bottom: 1px solid #292929 !important; 197 | background-color: rgb(32, 31, 31) !important; 198 | color: rgb(200, 198, 196) !important; 199 | } 200 | 201 | .header { 202 | color: #FFFFFF !important; 203 | } 204 | 205 | .main-component { 206 | .footer-container div { 207 | background: #292929 !important; 208 | } 209 | } 210 | } 211 | 212 | .highContrastContainer { 213 | 214 | .main-component { 215 | background: #0f0f0f !important; 216 | 217 | .form-content-container { 218 | .textstyle { 219 | color: rgb(200, 198, 196) !important; 220 | } 221 | } 222 | 223 | .textstyle { 224 | color: rgb(200, 198, 196) !important; 225 | } 226 | } 227 | 228 | .header { 229 | border-bottom: 1px solid #292929 !important; 230 | background-color: rgb(32, 31, 31) !important; 231 | 232 | .dark-theme { 233 | color: rgb(200, 198, 196) !important; 234 | } 235 | } 236 | 237 | .border-none { 238 | border-bottom: 1px solid #292929 !important; 239 | background-color: rgb(32, 31, 31) !important; 240 | color: rgb(200, 198, 196) !important; 241 | } 242 | 243 | .header { 244 | color: #FFFFFF !important; 245 | } 246 | 247 | .main-component { 248 | .footer-container div { 249 | background: #0f0f0f !important; 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/error-page/error-page.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | .error-message { 6 | text-align: center; 7 | width: 80%; 8 | height: 10rem; 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | margin: auto; 15 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/error-page/error-page.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from 'react'; 6 | import { RouteComponentProps } from 'react-router-dom'; 7 | import { Text } from '@stardust-ui/react'; 8 | import './error-page.scss'; 9 | import { WithTranslation, withTranslation } from "react-i18next"; 10 | import { TFunction } from "i18next"; 11 | 12 | 13 | interface IErrorPageProps extends WithTranslation, RouteComponentProps { 14 | } 15 | 16 | class ErrorPage extends React.Component { 17 | localize: TFunction; 18 | 19 | constructor(props: any) { 20 | super(props); 21 | this.localize = this.props.t; 22 | } 23 | 24 | public render(): JSX.Element { 25 | const params = this.props.match.params; 26 | let message = this.localize("generalErrorMessage"); 27 | if ("id" in params) { 28 | const id = params["id"]; 29 | if (id === "401") { 30 | message = this.localize("unauthorizedErrorMessage"); 31 | } else if (id === "403") { 32 | message = this.localize("forbiddenErrorMessage"); 33 | } 34 | else { 35 | message = this.localize("generalErrorMessage"); 36 | } 37 | } 38 | return ( 39 | 40 | ); 41 | } 42 | } 43 | 44 | export default withTranslation()(ErrorPage) 45 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/group-chat-warning/group-chat-warning.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | .blocktext { 6 | margin-left: auto; 7 | margin-right: auto; 8 | padding-right: 0.8rem; 9 | padding-left: 0.8rem; 10 | width: 8em 11 | } 12 | .footer-container { 13 | position: absolute; 14 | bottom: 0; 15 | left: 0; 16 | padding-right: 0.8rem; 17 | padding-left: 0.8rem; 18 | width: 100%; 19 | margin-bottom: 0.8rem; 20 | .button-container { 21 | float: right !important; 22 | } 23 | } 24 | .start-chat { 25 | margin-left: 1em !important; 26 | margin-right: 0.5rem !important; 27 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/group-chat-warning/group-chat-warning.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from 'react'; 6 | import { Button, Flex, ButtonProps } from '@stardust-ui/react'; 7 | import * as microsoftTeams from "@microsoft/teams-js"; 8 | import './group-chat-warning.scss'; 9 | import { WithTranslation, withTranslation } from "react-i18next"; 10 | import { TFunction } from "i18next"; 11 | 12 | export interface IGroupChatWarningProps extends WithTranslation{ 13 | chatListCount: string 14 | } 15 | 16 | class GroupChatWarning extends React.Component { 17 | localize: TFunction; 18 | constructor(props: IGroupChatWarningProps) { 19 | super(props); 20 | this.localize = this.props.t; 21 | this.onButtonClick = this.onButtonClick.bind(this); 22 | } 23 | 24 | //#region "React Life Cycle Hooks" 25 | public componentDidMount = () => { 26 | microsoftTeams.initialize(); 27 | } 28 | //#endregion 29 | 30 | //#region "On Button Click" 31 | private onButtonClick = (e: React.SyntheticEvent, v?: ButtonProps) => { 32 | microsoftTeams.tasks.submitTask({ "response": (e.currentTarget as Element).id }); 33 | } 34 | //#endregion 35 | 36 | public render(): JSX.Element { 37 | let styles = { padding: '5%' }; 38 | return ( 39 |
40 | 41 |
42 |

{this.localize("groupChatMessage")}

43 |

{this.localize("groupChatCountMessage", this.props.chatListCount)}

44 |

{this.localize("groupChatRecentMembers")}

45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 | ); 54 | } 55 | } 56 | export default withTranslation()(GroupChatWarning) -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/pagination/pagination.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from 'react'; 6 | import { List, FlexItem } from '@stardust-ui/react'; 7 | import { initializeIcons } from '@uifabric/icons'; 8 | 9 | export interface IPaginationProps { 10 | callbackFromParent: (newPageNumber: number) => void; 11 | entitiesLength: number; 12 | activePage: number; 13 | numberOfContents: number; 14 | } 15 | 16 | class Pagination extends React.Component { 17 | 18 | constructor(props: any) { 19 | super(props); 20 | initializeIcons(); 21 | }; 22 | 23 | public render(): JSX.Element { 24 | const numberOfPages = Math.ceil(this.props.entitiesLength / this.props.numberOfContents);//Total Page count 25 | 26 | //#region Populate paging List 27 | let pagingItems = [] 28 | pagingItems.push({ 29 | key: "<", 30 | header: "<", 31 | }); 32 | 33 | for (let k = 0; k < numberOfPages; k++) { 34 | pagingItems.push({ 35 | key: k, 36 | header: k + 1, 37 | }); 38 | } 39 | pagingItems.push({ 40 | key: ">", 41 | header: ">", 42 | }); 43 | //#endregion 44 | 45 | return ( 46 | 47 | 48 | { 51 | 52 | //#region "Handle Paging clicks" 53 | let seletedValue = this.props.activePage; 54 | if (newProps !== undefined && newProps.selectedIndex !== undefined) { 55 | 56 | if (newProps.selectedIndex > numberOfPages && this.props.activePage !== numberOfPages - 1)//If > is clicked 57 | { 58 | seletedValue = this.props.activePage + 1; 59 | newProps.selectedIndex = seletedValue; 60 | 61 | this.props.callbackFromParent(seletedValue); 62 | } 63 | else if (newProps.selectedIndex <= numberOfPages) //If < is clicked 64 | { 65 | if (newProps.selectedIndex !== 0 || this.props.activePage !== 0) { 66 | if (newProps.selectedIndex === 0) { 67 | seletedValue = this.props.activePage - 1; 68 | newProps.selectedIndex = seletedValue; 69 | } 70 | else 71 | seletedValue = newProps.selectedIndex - 1; 72 | 73 | this.props.callbackFromParent(seletedValue); 74 | } 75 | } 76 | } 77 | //#endregion "Handle Paging clicks" 78 | 79 | }} 80 | /> 81 | 82 | ); 83 | } 84 | } 85 | 86 | export default Pagination; -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/sign-in-page/sign-in-page.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | .sign-in-content-container { 6 | text-align: center; 7 | width: 80%; 8 | height: 10rem; 9 | position: absolute; 10 | top:0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | margin: auto; 15 | } 16 | 17 | .space { 18 | margin: 0.5rem; 19 | } 20 | 21 | .sign-in-button { 22 | line-height: normal 23 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/sign-in-page/sign-in-page.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { RouteComponentProps } from "react-router-dom"; 7 | import { Text, Button } from "@stardust-ui/react"; 8 | import * as microsoftTeams from "@microsoft/teams-js"; 9 | import "./sign-in-page.scss"; 10 | import { useTranslation } from "react-i18next" 11 | 12 | const SignInPage: React.FunctionComponent = props => { 13 | const localize = useTranslation().t; 14 | 15 | function onSignIn(): void { 16 | microsoftTeams.authentication.authenticate({ 17 | url: window.location.origin + "/signin-simple-start", 18 | successCallback: () => { 19 | console.log("Login succeeded!"); 20 | window.location.href = "/dls"; 21 | }, 22 | failureCallback: (reason) => { 23 | console.log("Login failed: " + reason); 24 | window.location.href = "/errorpage"; 25 | } 26 | }); 27 | } 28 | 29 | return ( 30 |
31 | 35 |
36 |
38 | ); 39 | }; 40 | 41 | export default SignInPage; 42 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/sign-in-page/sign-in-simple-end.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import React, { useEffect } from "react"; 6 | import * as microsoftTeams from "@microsoft/teams-js"; 7 | 8 | const SignInSimpleEnd: React.FunctionComponent = () => { 9 | // Parse hash parameters into key-value pairs 10 | function getHashParameters(): any { 11 | const hashParams: any = {}; 12 | window.location.hash.substr(1).split("&").forEach((item: string) => { 13 | let s = item.split("="), 14 | k = s[0], 15 | v = s[1] && decodeURIComponent(s[1]); 16 | hashParams[k] = v; 17 | }); 18 | return hashParams; 19 | } 20 | 21 | useEffect(() => { 22 | microsoftTeams.initialize(); 23 | 24 | const hashParams: any = getHashParameters(); 25 | if (hashParams["error"]) { 26 | // Authentication/authorization failed 27 | microsoftTeams.authentication.notifyFailure(hashParams["error"]); 28 | } else if (hashParams["id_token"]) { 29 | // Success 30 | microsoftTeams.authentication.notifySuccess(); 31 | } else { 32 | // Unexpected condition: hash does not contain error or access_token parameter 33 | microsoftTeams.authentication.notifyFailure("UnexpectedFailure"); 34 | } 35 | }); 36 | 37 | return ( 38 | <> 39 | ); 40 | }; 41 | 42 | export default SignInSimpleEnd; -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/components/sign-in-page/sign-in-simple-start.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import React, { useEffect } from "react"; 6 | import * as microsoftTeams from "@microsoft/teams-js"; 7 | import { getAuthenticationMetadata } from '../../apis/api-list'; 8 | 9 | const SignInSimpleStart: React.FunctionComponent = () => { 10 | useEffect(() => { 11 | microsoftTeams.initialize(); 12 | microsoftTeams.getContext(context => { 13 | const windowLocationOriginDomain = window.location.origin.replace("https://", ""); 14 | const loginHint = context.upn ? context.upn : ""; 15 | getAuthenticationMetadata(windowLocationOriginDomain, loginHint).then(result => { 16 | window.location.assign(result.data); 17 | }); 18 | }); 19 | }); 20 | 21 | return ( 22 | <> 23 | ); 24 | }; 25 | 26 | export default SignInSimpleStart; -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/configVariables.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | export const getBaseUrl = (): string => { 6 | return window.location.origin; 7 | } 8 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/i18n.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // 3 | // Copyright (c) Microsoft. All rights reserved. 4 | // 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var i18next_1 = require("i18next"); 7 | var react_i18next_1 = require("react-i18next"); 8 | var i18next_xhr_backend_1 = require("i18next-xhr-backend"); 9 | i18next_1.default 10 | .use(i18next_xhr_backend_1.default) 11 | .use(react_i18next_1.initReactI18next) // passes i18n down to react-i18next 12 | .init({ 13 | lng: window.navigator.language, 14 | fallbackLng: 'en-US', 15 | keySeparator: false, 16 | interpolation: { 17 | escapeValue: false // react already safes from xss 18 | } 19 | }); 20 | exports.default = i18next_1.default; 21 | //# sourceMappingURL=i18n.js.map -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/i18n.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"i18n.js","sourceRoot":"","sources":["i18n.ts"],"names":[],"mappings":";AAAA,iDAAiD;AACjD,gDAAgD;AAChD,eAAe;;AAEf,mCAA2B;AAC3B,+CAAiD;AACjD,2DAA0C;AAE1C,iBAAI;KACC,GAAG,CAAC,6BAAO,CAAC;KACZ,GAAG,CAAC,gCAAgB,CAAC,CAAC,oCAAoC;KAC1D,IAAI,CAAC;IACF,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,QAAQ;IAC9B,WAAW,EAAE,OAAO;IACpB,YAAY,EAAE,KAAK;IACnB,aAAa,EAAE;QACX,WAAW,EAAE,KAAK,CAAC,+BAA+B;KACrD;CACJ,CAAC,CAAC;AAGP,kBAAe,iBAAI,CAAC"} -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/i18n.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import i18n from "i18next"; 6 | import { initReactI18next } from "react-i18next"; 7 | import Backend from 'i18next-xhr-backend'; 8 | 9 | i18n 10 | .use(Backend) 11 | .use(initReactI18next) // passes i18n down to react-i18next 12 | .init({ 13 | lng: window.navigator.language, 14 | fallbackLng: 'en-US', 15 | keySeparator: false, // we do not use keys in form messages.welcome 16 | interpolation: { 17 | escapeValue: false // react already safes from xss 18 | } 19 | }); 20 | 21 | 22 | export default i18n; -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/index.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import './index.css'; 8 | import App from './App'; 9 | import { BrowserRouter as Router } from "react-router-dom"; 10 | 11 | //ReactDOM.render(, document.getElementById('root')); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , document.getElementById("root")); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/router/router.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // 3 | // Copyright (c) Microsoft. All rights reserved. 4 | // 5 | var __assign = (this && this.__assign) || function () { 6 | __assign = Object.assign || function(t) { 7 | for (var s, i = 1, n = arguments.length; i < n; i++) { 8 | s = arguments[i]; 9 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 10 | t[p] = s[p]; 11 | } 12 | return t; 13 | }; 14 | return __assign.apply(this, arguments); 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | var React = require("react"); 18 | var react_1 = require("react"); 19 | var react_router_dom_1 = require("react-router-dom"); 20 | var error_page_1 = require("../components/error-page/error-page"); 21 | var sign_in_page_1 = require("../components/sign-in-page/sign-in-page"); 22 | var sign_in_simple_start_1 = require("../components/sign-in-page/sign-in-simple-start"); 23 | var sign_in_simple_end_1 = require("../components/sign-in-page/sign-in-simple-end"); 24 | var distribution_lists_1 = require("../components/distribution-lists/distribution-lists"); 25 | var add_distribution_list_1 = require("../components/add-distribution-list/add-distribution-list"); 26 | var distribution_list_members_1 = require("../components/distribution-list-members/distribution-list-members"); 27 | var group_chat_warning_1 = require("../components/group-chat-warning/group-chat-warning"); 28 | var api_list_1 = require("../apis/api-list"); 29 | require("../i18n"); 30 | exports.AppRoute = function () { 31 | return (React.createElement(react_1.Suspense, { fallback: React.createElement(React.Fragment, null) }, 32 | React.createElement(react_router_dom_1.BrowserRouter, null, 33 | React.createElement(react_router_dom_1.Switch, null, 34 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/dls", render: function (props) { return React.createElement(distribution_lists_1.default, __assign({}, props, { getFavoriteDistributionLists: api_list_1.getFavoriteDistributionLists, getDistributionListMembersOnlineCount: api_list_1.getDistributionListMembersOnlineCount, getUserPageSizeChoice: api_list_1.getUserPageSizeChoice, createUserPageSizeChoice: api_list_1.createUserPageSizeChoice, getClientId: api_list_1.getClientId })); } }), 35 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/dlmemberlist/:id/:name", render: function (props) { return React.createElement(distribution_list_members_1.default, __assign({}, props, { parentDlId: props.match.params.id, parentDLName: props.match.params.name, getDistributionListsMembers: api_list_1.getDistributionListsMembers, pinStatusUpdate: api_list_1.pinStatusUpdate, getUserPresence: api_list_1.getUserPresence, getUserPageSizeChoice: api_list_1.getUserPageSizeChoice, createUserPageSizeChoice: api_list_1.createUserPageSizeChoice })); } }), 36 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/adfavorite/:isskypedl?", render: function (props) { return React.createElement(add_distribution_list_1.default, __assign({}, props, { getADDistributionLists: api_list_1.getADDistributionLists, createFavoriteDistributionList: api_list_1.createFavoriteDistributionList })); } }), 37 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/groupchatwarning/:count", render: function (props) { return React.createElement(group_chat_warning_1.default, __assign({}, props, { chatListCount: props.match.params.count })); } }), 38 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/errorpage", component: error_page_1.default }), 39 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/errorpage/:id", component: error_page_1.default }), 40 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/signin", component: sign_in_page_1.default }), 41 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/signin-simple-start", component: sign_in_simple_start_1.default }), 42 | React.createElement(react_router_dom_1.Route, { exact: true, path: "/signin-simple-end", component: sign_in_simple_end_1.default }))))); 43 | }; 44 | //# sourceMappingURL=router.js.map -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/router/router.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"router.js","sourceRoot":"","sources":["router.tsx"],"names":[],"mappings":";AAAA,oDAAoD;AACpD,gDAAgD;AAChD,eAAe;;;;;;;;;;;;;AAEf,6BAA+B;AAC/B,+BAAiC;AACjC,qDAAgE;AAChE,kEAA4D;AAC5D,wEAAiE;AACjE,wFAAgF;AAChF,oFAA4E;AAC5E,0FAAoF;AACpF,mGAA4F;AAC5F,+GAAwG;AACxG,0FAAmF;AACnF,6CAA4R;AAC5R,mBAAiB;AAEJ,QAAA,QAAQ,GAAgC;IAEjD,OAAO,CACH,oBAAC,gBAAQ,IAAC,QAAQ,EAAE,yCAAK;QACrB,oBAAC,gCAAa;YACV,oBAAC,yBAAM;gBACH,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,MAAM,EAAC,MAAM,EAAE,UAAC,KAAK,IAAK,OAAA,oBAAC,4BAAiB,eAAK,KAAK,IAAE,4BAA4B,EAAE,uCAA4B,EAAE,qCAAqC,EAAE,gDAAqC,EAAE,qBAAqB,EAAE,gCAAqB,EAAE,wBAAwB,EAAE,mCAAwB,EAAE,WAAW,EAAE,sBAAW,IAAI,EAAtS,CAAsS,GAAI;gBACtV,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,yBAAyB,EAAC,MAAM,EAAE,UAAC,KAAK,IAAK,OAAA,oBAAC,mCAAuB,eAAK,KAAK,IAAE,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,2BAA2B,EAAE,sCAA2B,EAAE,eAAe,EAAE,0BAAe,EAAE,eAAe,EAAE,0BAAe,EAAE,qBAAqB,EAAE,gCAAqB,EAAE,wBAAwB,EAAE,mCAAwB,IAAI,EAAhV,CAAgV,GAAI;gBACnZ,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,yBAAyB,EAAC,MAAM,EAAE,UAAC,KAAK,IAAK,OAAA,oBAAC,+BAAmB,eAAK,KAAK,IAAE,sBAAsB,EAAE,iCAAsB,EAAE,8BAA8B,EAAE,yCAA8B,IAAI,EAAlJ,CAAkJ,GAAI;gBACrN,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,0BAA0B,EAAC,MAAM,EAAE,UAAC,KAAK,IAAK,OAAA,oBAAC,4BAAgB,eAAK,KAAK,IAAE,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,EAAxE,CAAwE,GAAI;gBAC5I,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,YAAY,EAAC,SAAS,EAAE,oBAAS,GAAI;gBACvD,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,gBAAgB,EAAC,SAAS,EAAE,oBAAS,GAAI;gBAC3D,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,SAAS,EAAC,SAAS,EAAE,sBAAU,GAAI;gBACrD,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAE,8BAAiB,GAAI;gBACzE,oBAAC,wBAAK,IAAC,KAAK,QAAC,IAAI,EAAC,oBAAoB,EAAC,SAAS,EAAE,4BAAe,GAAI,CAChE,CACG,CACT,CACd,CAAC;AACN,CAAC,CAAA"} -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/src/router/router.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from 'react'; 6 | import { Suspense } from 'react'; 7 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 8 | import ErrorPage from "../components/error-page/error-page"; 9 | import SignInPage from "../components/sign-in-page/sign-in-page"; 10 | import SignInSimpleStart from "../components/sign-in-page/sign-in-simple-start"; 11 | import SignInSimpleEnd from "../components/sign-in-page/sign-in-simple-end"; 12 | import DistributionLists from '../components/distribution-lists/distribution-lists'; 13 | import AddDistributionList from '../components/add-distribution-list/add-distribution-list'; 14 | import DistributionListMembers from '../components/distribution-list-members/distribution-list-members'; 15 | import GroupChatWarning from '../components/group-chat-warning/group-chat-warning'; 16 | import { createFavoriteDistributionList, getADDistributionLists, pinStatusUpdate, getDistributionListsMembers, getFavoriteDistributionLists, getDistributionListMembersOnlineCount, getUserPresence, getUserPageSizeChoice, createUserPageSizeChoice, getClientId } from '../apis/api-list'; 17 | import "../i18n"; 18 | 19 | export const AppRoute: React.FunctionComponent<{}> = () => { 20 | 21 | return ( 22 | }> 23 | 24 | 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "preserve" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Constants/PresenceStates.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Constants 6 | { 7 | /// 8 | /// Maintains the Presence state constant strings supported by Presence Graph APIs 9 | /// Refer the list of base presence information https://docs.microsoft.com/en-us/graph/api/resources/presence?view=graph-rest-beta#properties 10 | /// Possible values are Available, AvailableIdle, Away, BeRightBack, Busy, BusyIdle, DoNotDisturb, Offline, PresenceUnknown 11 | /// 12 | public class PresenceStates 13 | { 14 | /// 15 | /// Represents available presence state 16 | /// 17 | public const string Available = "Available"; 18 | 19 | /// 20 | /// Represents away presence state 21 | /// 22 | public const string Away = "Away"; 23 | 24 | /// 25 | /// Represents busy presence state 26 | /// 27 | public const string Busy = "Busy"; 28 | 29 | /// 30 | /// Represents be right back presence state 31 | /// 32 | public const string BeRightBack = "BeRightBack"; 33 | 34 | /// 35 | /// Represents do not disturb presence state 36 | /// 37 | public const string DoNotDisturb = "DoNotDisturb"; 38 | 39 | /// 40 | /// Represents offline presence state 41 | /// 42 | public const string Offline = "Offline"; 43 | 44 | /// 45 | /// Represents presence unknown state 46 | /// 47 | public const string PresenceUnknown = "PresenceUnknown"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Controllers/AuthenticationMetadataController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Controllers 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Web; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Teams.Apps.DLLookup.Models; 14 | 15 | /// 16 | /// Controller for the authentication sign in data. 17 | /// 18 | [Route("api/authenticationMetadata")] 19 | public class AuthenticationMetadataController : ControllerBase 20 | { 21 | private readonly string tenantId; 22 | private readonly string clientId; 23 | private readonly string graphScope; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// Instance of application configuration. 29 | public AuthenticationMetadataController(IConfiguration configuration) 30 | { 31 | this.tenantId = configuration["AzureAd:TenantId"]; 32 | this.clientId = configuration["AzureAd:ClientId"]; 33 | this.graphScope = configuration["AzureAd:GraphScope"]; 34 | } 35 | 36 | /// 37 | /// Get authentication URL with configuration options. 38 | /// 39 | /// Instance of athentication info model to get window origin and login hint details. 40 | /// Consent URL. 41 | [HttpPost("GetAuthenticationUrlWithConfiguration")] 42 | public string GetAuthenticationUrlWithConfiguration([FromBody] AuthenticationInfo authenticationInfo) 43 | { 44 | Dictionary authDictionary = new Dictionary 45 | { 46 | ["redirect_uri"] = $"https://{authenticationInfo.WindowLocationOriginDomain}/signin-simple-end", 47 | ["client_id"] = this.clientId, 48 | ["response_type"] = "id_token", 49 | ["response_mode"] = "fragment", 50 | ["scope"] = this.graphScope, 51 | ["nonce"] = Guid.NewGuid().ToString(), 52 | ["state"] = Guid.NewGuid().ToString(), 53 | ["login_hint"] = authenticationInfo.LoginHint, 54 | }; 55 | List authList = authDictionary 56 | .Select(p => $"{p.Key}={HttpUtility.UrlEncode(p.Value)}") 57 | .ToList(); 58 | 59 | string authUrlPrefix = $"https://login.microsoftonline.com/{this.tenantId}/oauth2/v2.0/authorize?"; 60 | 61 | string authUrlWithConfigUrlString = authUrlPrefix + string.Join('&', authList); 62 | 63 | return authUrlWithConfigUrlString; 64 | } 65 | 66 | /// 67 | /// Gets the application client Id. 68 | /// 69 | /// Application client Id. 70 | [HttpGet("GetClientId")] 71 | public string GetClientId() 72 | { 73 | return this.clientId; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Controllers 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Net.Http.Headers; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Mvc; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Options; 15 | using Microsoft.Identity.Client; 16 | using Microsoft.Teams.Apps.DLLookup.Helpers; 17 | using Microsoft.Teams.Apps.DLLookup.Models; 18 | 19 | /// 20 | /// Base controller to handle token generation. 21 | /// 22 | [Route("api/[controller]")] 23 | [ApiController] 24 | public class BaseController : ControllerBase 25 | { 26 | private readonly IOptions azureAdOptions; 27 | private readonly ILogger logger; 28 | private readonly IConfidentialClientApplication confidentialClientApp; 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// Instance of ConfidentialClientApplication class. 34 | /// Instance of IOptions to read data from application configuration. 35 | /// Instance to send logs to the Application Insights service. 36 | public BaseController( 37 | IConfidentialClientApplication confidentialClientApp, 38 | IOptions azureAdOptions, 39 | ILogger logger) 40 | { 41 | this.confidentialClientApp = confidentialClientApp; 42 | this.azureAdOptions = azureAdOptions; 43 | this.logger = logger; 44 | } 45 | 46 | /// 47 | /// Gets user's Azure AD object id. 48 | /// 49 | public string UserObjectId 50 | { 51 | get 52 | { 53 | var oidClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier"; 54 | var claim = this.User.Claims.First(p => oidClaimType.Equals(p.Type, StringComparison.Ordinal)); 55 | return claim.Value; 56 | } 57 | } 58 | 59 | /// 60 | /// Get user Azure AD access token. 61 | /// 62 | /// Token to access MS graph. 63 | public async Task GetAccessTokenAsync() 64 | { 65 | List scopeList = this.azureAdOptions.Value.GraphScope.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries).ToList(); 66 | 67 | try 68 | { 69 | // Gets user account from the accounts available in token cache. 70 | // https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.clientapplicationbase.getaccountasync?view=azure-dotnet 71 | // Concatenation of UserObjectId and TenantId separated by a dot is used as unique identifier for getting user account. 72 | // https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.accountid.identifier?view=azure-dotnet#Microsoft_Identity_Client_AccountId_Identifier 73 | var account = await this.confidentialClientApp.GetAccountAsync($"{this.UserObjectId}.{this.azureAdOptions.Value.TenantId}"); 74 | 75 | // Attempts to acquire an access token for the account from the user token cache. 76 | // https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.clientapplicationbase.acquiretokensilent?view=azure-dotnet 77 | AuthenticationResult result = await this.confidentialClientApp 78 | .AcquireTokenSilent(scopeList, account) 79 | .ExecuteAsync(); 80 | return result.AccessToken; 81 | } 82 | catch (MsalUiRequiredException msalex) 83 | { 84 | // Getting new token using AddTokenToCacheFromJwtAsync as AcquireTokenSilent failed to load token from cache. 85 | TokenAcquisitionHelper tokenAcquisitionHelper = new TokenAcquisitionHelper(this.confidentialClientApp); 86 | try 87 | { 88 | this.logger.LogInformation($"MSAL exception occurred while trying to acquire new token. MSAL exception details are found {msalex}."); 89 | var jwtToken = AuthenticationHeaderValue.Parse(this.Request.Headers["Authorization"].ToString()).Parameter; 90 | return await tokenAcquisitionHelper.AddTokenToCacheFromJwtAsync(this.azureAdOptions.Value.GraphScope, jwtToken); 91 | } 92 | catch (Exception ex) 93 | { 94 | this.logger.LogError(ex, $"An error occurred in GetAccessTokenAsync: {ex.Message}."); 95 | throw; 96 | } 97 | } 98 | catch (Exception ex) 99 | { 100 | this.logger.LogError(ex, $"An error occurred in fetching token : {ex.Message}."); 101 | throw; 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Controllers/DistributionListMembersController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Controllers 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Authorization; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Logging; 13 | using Microsoft.Extensions.Options; 14 | using Microsoft.Identity.Client; 15 | using Microsoft.Teams.Apps.DLLookup.Models; 16 | using Microsoft.Teams.Apps.DLLookup.Repositories; 17 | using Microsoft.Teams.Apps.DLLookup.Repositories.Interfaces; 18 | 19 | /// 20 | /// Creating class with ControllerBase as base class. Controller for Distribution List member APIs. 21 | /// 22 | [Authorize] 23 | [Route("api/[controller]")] 24 | [ApiController] 25 | public class DistributionListMembersController : BaseController 26 | { 27 | private readonly IFavoriteDistributionListMemberDataRepository favoriteDistributionListMemberDataRepository; 28 | private readonly ILogger logger; 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// Scoped FavoriteDistributionListMemberDataRepository instance used to read/write distribution list member related operations. 34 | /// Instance of IOptions to read data from application configuration. 35 | /// Instance to send logs to the Application Insights service. 36 | /// Instance of ConfidentialClientApplication class. 37 | public DistributionListMembersController( 38 | IFavoriteDistributionListMemberDataRepository favoriteDistributionListMemberDataRepository, 39 | IOptions azureAdOptions, 40 | ILogger logger, 41 | IConfidentialClientApplication confidentialClientApp) 42 | : base(confidentialClientApp, azureAdOptions, logger) 43 | { 44 | this.favoriteDistributionListMemberDataRepository = favoriteDistributionListMemberDataRepository; 45 | this.logger = logger; 46 | } 47 | 48 | /// 49 | /// Gets the members in a Distribution List using the group GUID from Graph API. 50 | /// 51 | /// Distribution list group GUID. 52 | /// A list of Distribution List members information. 53 | [HttpGet] 54 | public async Task GetMembersAsync([FromQuery] string groupId) 55 | { 56 | try 57 | { 58 | if (groupId == null || groupId.Length == 0) 59 | { 60 | return this.BadRequest("Post query data is either null or empty."); 61 | } 62 | 63 | string accessToken = await this.GetAccessTokenAsync(); 64 | List distributionListMembers = await this.favoriteDistributionListMemberDataRepository 65 | .GetMembersAsync(groupId, accessToken, this.UserObjectId); 66 | return this.Ok(distributionListMembers); 67 | } 68 | catch (Exception ex) 69 | { 70 | this.logger.LogError(ex, $"An error occurred in GetMembersAsync: {ex.Message}, Parameters:{groupId}"); 71 | throw; 72 | } 73 | } 74 | 75 | /// 76 | /// Adds member data to the table storage on being pinned by the user. 77 | /// 78 | /// Instance of favorite Distribution List member data holding the values sent by the user. 79 | /// A representing the asynchronous operation. 80 | [HttpPost] 81 | public async Task CreateFavoriteDistributionMemberListDataAsync([FromBody] FavoriteDistributionListMemberData favoriteDistributionListMemberData) 82 | { 83 | try 84 | { 85 | favoriteDistributionListMemberData.UserObjectId = this.UserObjectId; 86 | await this.favoriteDistributionListMemberDataRepository.AddFavoriteDistributionListMemberAsync(favoriteDistributionListMemberData); 87 | 88 | return this.Ok(); 89 | } 90 | catch (Exception ex) 91 | { 92 | this.logger.LogError(ex, $"An error occurred in CreateFavoriteDistributionMemberListDataAsync: {ex.Message}"); 93 | throw; 94 | } 95 | } 96 | 97 | /// 98 | /// Updates azure table storage when user unpins their favorite members. 99 | /// 100 | /// Instance of FavoriteDistributionListMemberData holding the values sent by the user for unpin. 101 | /// A representing the asynchronous operation. 102 | [HttpDelete] 103 | public async Task DeleteFavoriteDistributionListMemberDataAsync([FromBody] FavoriteDistributionListMemberData favoriteDistributionListMemberData) 104 | { 105 | try 106 | { 107 | favoriteDistributionListMemberData.UserObjectId = this.UserObjectId; 108 | 109 | FavoriteDistributionListMemberTableEntity favoriteDistributionListMemberDataEntity = await this.favoriteDistributionListMemberDataRepository 110 | .GetFavoriteMemberFromStorageAsync(favoriteDistributionListMemberData.PinnedUserId + favoriteDistributionListMemberData.DistributionListId, favoriteDistributionListMemberData.UserObjectId); 111 | 112 | if (favoriteDistributionListMemberDataEntity != null) 113 | { 114 | await this.favoriteDistributionListMemberDataRepository.DeleteFavoriteMemberFromStorageAsync(favoriteDistributionListMemberDataEntity); 115 | } 116 | 117 | return this.Ok(); 118 | } 119 | catch (Exception ex) 120 | { 121 | this.logger.LogError(ex, $"An error occurred in DeleteFavoriteDistributionListMemberDataAsync: {ex.Message}"); 122 | throw; 123 | } 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Controllers/PresenceController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Controllers 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Authorization; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Microsoft.Identity.Client; 14 | using Microsoft.Teams.Apps.DLLookup.Models; 15 | using Microsoft.Teams.Apps.DLLookup.Repositories; 16 | 17 | /// 18 | /// creating class with ControllerBase as base class. Controller for user presence APIs. 19 | /// 20 | [Authorize] 21 | [Route("api/[controller]")] 22 | [ApiController] 23 | public class PresenceController : BaseController 24 | { 25 | private readonly IPresenceDataRepository presenceDataRepository; 26 | private readonly ILogger logger; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// Scoped PresenceDataRepository instance used to get presence information. 32 | /// Instance of IOptions to read data from application configuration. 33 | /// Instance to send logs to the Application Insights service. 34 | /// Instance of ConfidentialClientApplication class. 35 | public PresenceController( 36 | IPresenceDataRepository presenceDataRepository, 37 | IConfidentialClientApplication confidentialClientApp, 38 | IOptions azureAdOptions, 39 | ILogger logger) 40 | : base(confidentialClientApp, azureAdOptions, logger) 41 | { 42 | this.presenceDataRepository = presenceDataRepository; 43 | this.logger = logger; 44 | } 45 | 46 | /// 47 | /// Get User presence status details. 48 | /// 49 | /// Array of People Presence Data object used to get presence information. 50 | /// People Presence Data model data filled with presence information. 51 | [HttpPost] 52 | [Route("GetUserPresence")] 53 | public async Task GetUserPresenceAsync([FromBody]PeoplePresenceData[] peoplePresenceData) 54 | { 55 | try 56 | { 57 | string accessToken = await this.GetAccessTokenAsync(); 58 | return this.Ok(await this.presenceDataRepository.GetBatchUserPresenceAsync(peoplePresenceData, accessToken)); 59 | } 60 | catch (Exception ex) 61 | { 62 | this.logger.LogError(ex, "An error occurred while getting user presence details."); 63 | throw; 64 | } 65 | } 66 | 67 | /// 68 | /// Gets online members count in a distribution list. 69 | /// 70 | /// Distribution list group GUID. 71 | /// Online members count in distribution list. 72 | [HttpGet] 73 | [Route("GetDistributionListMembersOnlineCount")] 74 | public async Task GetDistributionListMembersOnlineCountAsync([FromQuery]string groupId) 75 | { 76 | try 77 | { 78 | string accessToken = await this.GetAccessTokenAsync(); 79 | return this.Ok(await this.presenceDataRepository.GetDistributionListMembersOnlineCountAsync(groupId, accessToken)); 80 | } 81 | catch (Exception ex) 82 | { 83 | this.logger.LogError(ex, $"An error occurred in GetDistributionListMembersOnlineCountAsync: {ex.Message}"); 84 | throw; 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Controllers/UserPageSizeController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Controllers 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Authorization; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Microsoft.Identity.Client; 14 | using Microsoft.Teams.Apps.DLLookup.Models; 15 | using Microsoft.Teams.Apps.DLLookup.Repositories; 16 | 17 | /// 18 | /// creating class with ControllerBase as base class. Controller for page size APIs. 19 | /// 20 | [Authorize] 21 | [Route("api/UserPageSize")] 22 | [ApiController] 23 | public class UserPageSizeController : BaseController 24 | { 25 | private readonly UserPageSizeChoiceDataRepository userPageSizeChoiceDataRepository; 26 | private readonly ILogger logger; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// Instance of ConfidentialClientApplication class. 32 | /// Instance of IOptions to read data from application configuration. 33 | /// Singleton UserPageSizeChoiceDataRepository instance used to perform read/store operations for page size. 34 | /// Instance to send logs to the Application Insights service. 35 | public UserPageSizeController( 36 | UserPageSizeChoiceDataRepository userPageSizeChoiceDataRepository, 37 | IConfidentialClientApplication confidentialClientApp, 38 | IOptions azureAdOptions, 39 | ILogger logger) 40 | : base(confidentialClientApp, azureAdOptions, logger) 41 | { 42 | this.userPageSizeChoiceDataRepository = userPageSizeChoiceDataRepository; 43 | this.logger = logger; 44 | } 45 | 46 | /// 47 | /// Gets the page size values for currently logged in user from database. 48 | /// 49 | /// A representing user page size. 50 | [HttpGet] 51 | public async Task GetUserPageSizeChoiceAsync() 52 | { 53 | try 54 | { 55 | return this.Ok(await this.userPageSizeChoiceDataRepository.GetUserPageSizeChoice(this.UserObjectId)); 56 | } 57 | catch (Exception ex) 58 | { 59 | this.logger.LogError(ex, $"An error occurred in getUserPageSizeChoice: {ex.Message}. Property: {this.UserObjectId}"); 60 | throw; 61 | } 62 | } 63 | 64 | /// 65 | /// Stores page size values in database for currently logged in user. 66 | /// 67 | /// Page size to be stored. 68 | /// A representing the asynchronous operation. 69 | [HttpPost] 70 | public async Task CreateUserPageSizeChoiceAsync([FromBody] UserPageSizeChoice userPageSizeChoice) 71 | { 72 | try 73 | { 74 | await this.userPageSizeChoiceDataRepository.CreateOrUpdateUserPageSizeChoiceDataAsync(userPageSizeChoice.PageSize, userPageSizeChoice.PageId, this.UserObjectId); 75 | return this.Ok(); 76 | } 77 | catch (Exception ex) 78 | { 79 | this.logger.LogError(ex, $"An error occurred in CreateUserPageSizeChoiceAsync: {ex.Message}. UserObjectId:{this.UserObjectId}"); 80 | throw; 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Helpers/Extensions/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Helpers.Extentions 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | /// 11 | /// Class providing extension methods for List type. 12 | /// 13 | public static class ListExtensions 14 | { 15 | /// 16 | /// This method is to split list into given batch size. 17 | /// 18 | /// T type. 19 | /// Source list to split. 20 | /// Size value to split the list with 40 as default value. 21 | /// A representing the sub-lists by specified size. 22 | public static IEnumerable> SplitList(this List source, int nSize = 40) 23 | { 24 | for (int i = 0; i < source.Count; i += nSize) 25 | { 26 | yield return source.GetRange(i, Math.Min(nSize, source.Count - i)); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Helpers/Extensions/RepositoriesServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Helpers.Extentions 6 | { 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Teams.Apps.DLLookup.Repositories; 9 | using Microsoft.Teams.Apps.DLLookup.Repositories.Interfaces; 10 | 11 | /// 12 | /// Class to add DI services for repositories. 13 | /// 14 | public static class RepositoriesServiceCollectionExtensions 15 | { 16 | /// 17 | /// Extension method to register repository services in DI container. 18 | /// 19 | /// IServiceCollection instance to which repository services to be added in. 20 | public static void AddRepositories(this IServiceCollection services) 21 | { 22 | services.AddScoped(); 23 | services.AddScoped(); 24 | services.AddScoped(); 25 | services.AddSingleton(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Helpers/TokenAcquisitionHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Helpers 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Microsoft.Identity.Client; 12 | 13 | /// 14 | /// Gets and sets access token in cache. 15 | /// 16 | public class TokenAcquisitionHelper 17 | { 18 | private readonly IConfidentialClientApplication confidentialClientApp; 19 | private readonly string[] scopesRequestedByMsalNet = new string[] 20 | { 21 | "openid", 22 | "profile", 23 | "offline_access", 24 | }; 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// Instance of ConfidentialClientApplication class. 30 | public TokenAcquisitionHelper( 31 | IConfidentialClientApplication confidentialClientApp) 32 | { 33 | this.confidentialClientApp = confidentialClientApp; 34 | } 35 | 36 | /// 37 | /// Adds token to cache. 38 | /// 39 | /// Graph scopes to be added to token. 40 | /// JWT bearer token. 41 | /// Token with graph scopes. 42 | public async Task AddTokenToCacheFromJwtAsync(string graphScopes, string jwtToken) 43 | { 44 | if (jwtToken == null) 45 | { 46 | throw new ArgumentNullException(jwtToken, "tokenValidationContext.SecurityToken should be a JWT Token"); 47 | } 48 | 49 | UserAssertion userAssertion = new UserAssertion(jwtToken, "urn:ietf:params:oauth:grant-type:jwt-bearer"); 50 | IEnumerable requestedScopes = graphScopes.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries).ToList(); 51 | 52 | // Result to make sure that the cache is filled-in before the controller tries to get access tokens 53 | var result = await this.confidentialClientApp.AcquireTokenOnBehalfOf( 54 | requestedScopes.Except(this.scopesRequestedByMsalNet), 55 | userAssertion) 56 | .ExecuteAsync(); 57 | return result.AccessToken; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Microsoft.Teams.Apps.DLLookup.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | latest 10 | $(DefaultItemExcludes);$(SpaRoot)node_modules\**;.git\** 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | BetaLib 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | %(DistFiles.Identity) 74 | PreserveNewest 75 | 76 | 77 | 78 | 79 | 80 | $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), .gitignore))\Build\Analyzer.ruleset 81 | bin\$(Configuration)\$(Platform)\$(AssemblyName).xml 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/AuthenticationInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | /// 8 | /// AuthenticationInfo model represents information required to get athentication Uri. 9 | /// 10 | public class AuthenticationInfo 11 | { 12 | /// 13 | /// Gets or sets window location domain origin. 14 | /// 15 | public string WindowLocationOriginDomain { get; set; } 16 | 17 | /// 18 | /// Gets or sets login hint details. 19 | /// 20 | public string LoginHint { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/AzureAdOptions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | /// 8 | /// AzureAdOptions class contain value application configuration properties for Azure Active Directory. 9 | /// 10 | public class AzureAdOptions 11 | { 12 | /// 13 | /// Gets or sets Client Id. 14 | /// 15 | public string ClientId { get; set; } 16 | 17 | /// 18 | /// Gets or sets Client secret. 19 | /// 20 | public string ClientSecret { get; set; } 21 | 22 | /// 23 | /// Gets or sets Graph API scope. 24 | /// 25 | public string GraphScope { get; set; } 26 | 27 | /// 28 | /// Gets or sets Application Id URI. 29 | /// 30 | public string ApplicationIdUri { get; set; } 31 | 32 | /// 33 | /// Gets or sets valid isuers. 34 | /// 35 | public string ValidIssuers { get; set; } 36 | 37 | /// 38 | /// Gets or sets tenant Id. 39 | /// 40 | public string TenantId { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/CacheOptions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | /// 8 | /// This class contain value application configuration properties for Cache. 9 | /// 10 | public class CacheOptions 11 | { 12 | /// 13 | /// Gets or sets cache interval. 14 | /// 15 | public int CacheInterval { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/DistributionList.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | using Newtonsoft.Json; 8 | 9 | /// 10 | /// DistributionList model is for distribution lists data from AAD and table storage. 11 | /// 12 | public class DistributionList 13 | { 14 | /// 15 | /// Gets or sets the Id from AAD for a particular distribution list. 16 | /// 17 | [JsonProperty("id")] 18 | public string Id { get; set; } 19 | 20 | /// 21 | /// Gets or sets the display name from AAD for a particular distribution list. 22 | /// 23 | [JsonProperty("displayName")] 24 | public string DisplayName { get; set; } 25 | 26 | /// 27 | /// Gets or sets the mail from AAD for a particular distribution list. 28 | /// 29 | [JsonProperty("mail")] 30 | public string Mail { get; set; } 31 | 32 | /// 33 | /// Gets or sets the mail nickname from AAD for a particular distribution list. 34 | /// 35 | [JsonProperty("mailNickname")] 36 | public string MailNickname { get; set; } 37 | 38 | /// 39 | /// Gets or sets the mail enabled from AAD for a particular distribution list. 40 | /// 41 | [JsonProperty("mailEnabled")] 42 | public string MailEnabled { get; set; } 43 | 44 | /// 45 | /// Gets or sets the number of members in a particular distribution list. 46 | /// 47 | [JsonProperty("noOfMembers")] 48 | public int MembersCount { get; set; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/DistributionListMember.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | using Newtonsoft.Json; 8 | 9 | /// 10 | /// DistributionListMember model is for distribution list members data from AAD and table storage. 11 | /// 12 | public class DistributionListMember 13 | { 14 | /// 15 | /// Gets or sets odata type property for a given distribution list member. 16 | /// 17 | [JsonProperty("@odata.type")] 18 | public string OData_Type { get; set; } 19 | 20 | /// 21 | /// Gets Type property which indicates whether the member is a nested distributed list or a contact. 22 | /// 23 | [JsonProperty("type")] 24 | public string Type 25 | { 26 | get { return this.OData_Type; } 27 | } 28 | 29 | /// 30 | /// Gets or sets UserType property which indicates whether the member is a guest or not. 31 | /// 32 | [JsonProperty("userType")] 33 | public string UserType { get; set; } 34 | 35 | /// 36 | /// Gets or sets id of the corresponding distribution list member. 37 | /// 38 | [JsonProperty("id")] 39 | public string UserObjectId { get; set; } 40 | 41 | /// 42 | /// Gets or sets display name of the corresponding distribution list member. 43 | /// 44 | [JsonProperty("displayName")] 45 | public string DisplayName { get; set; } 46 | 47 | /// 48 | /// Gets or sets mail of the corresponding distribution list member. 49 | /// 50 | [JsonProperty("mail")] 51 | public string Mail { get; set; } 52 | 53 | /// 54 | /// Gets or sets user principal name of the corresponding distribution list member. 55 | /// 56 | [JsonProperty("userPrincipalName")] 57 | public string UserPrincipalName { get; set; } 58 | 59 | /// 60 | /// Gets or sets job title of the corresponding distribution list member. 61 | /// 62 | [JsonProperty("jobTitle")] 63 | public string JobTitle { get; set; } 64 | 65 | /// 66 | /// Gets or sets a value indicating whether record is pinned or not by the logged in user. 67 | /// 68 | public bool IsPinned { get; set; } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/FavoriteDistributionListData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | /// 8 | /// This class represents user favorite distribution lists. 9 | /// 10 | public class FavoriteDistributionListData 11 | { 12 | /// 13 | /// Gets or sets Id of the distribution lists in the favorites list. 14 | /// 15 | public string Id { get; set; } 16 | 17 | /// 18 | /// Gets or sets a value indicating whether record is pinned or not by the logged in user. 19 | /// 20 | public bool IsPinned { get; set; } 21 | 22 | /// 23 | /// Gets or sets display name of the distribution lists in the favorites list. 24 | /// 25 | public string DisplayName { get; set; } 26 | 27 | /// 28 | /// Gets or sets alias of the distribution lists in the favorites list. 29 | /// 30 | public string Mail { get; set; } 31 | 32 | /// 33 | /// Gets or sets number of contacts of the distribution lists in the favorites list. 34 | /// 35 | public int ContactsCount { get; set; } 36 | 37 | /// 38 | /// Gets or sets user object id of the distribution lists in the favorites list. 39 | /// 40 | public string UserObjectId { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/FavoriteDistributionListMemberData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | /// 8 | /// FavoriteDistributionListMemberData model represents favorite distribution list member data. 9 | /// 10 | public class FavoriteDistributionListMemberData 11 | { 12 | /// 13 | /// Gets or sets user id of the favorite member in the distributed list. 14 | /// 15 | public string PinnedUserId { get; set; } 16 | 17 | /// 18 | /// Gets or sets distribution list GUID, the pinned member belongs to. 19 | /// 20 | public string DistributionListId { get; set; } 21 | 22 | /// 23 | /// Gets or sets user object Id. 24 | /// 25 | public string UserObjectId { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/FavoriteDistributionListMemberTableEntity.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using Microsoft.WindowsAzure.Storage.Table; 8 | 9 | /// 10 | /// Favorite Distribution List Member Data table entity class represents pinned member records. 11 | /// 12 | public class FavoriteDistributionListMemberTableEntity : TableEntity 13 | { 14 | /// 15 | /// Gets or sets pinned record's distribution list GUID. 16 | /// 17 | public string DistributionListId { get; set; } 18 | 19 | /// 20 | /// Gets or sets Partition key with users's object id. 21 | /// 22 | [IgnoreProperty] 23 | public string UserObjectId 24 | { 25 | get { return this.PartitionKey; } 26 | set { this.PartitionKey = value; } 27 | } 28 | 29 | /// 30 | /// Gets or sets row key with pinned record id + Distribution list id. 31 | /// 32 | [IgnoreProperty] 33 | public string DistributionListMemberId 34 | { 35 | get { return this.RowKey; } 36 | set { this.RowKey = value; } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/FavoriteDistributionListTableEntity.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using Microsoft.WindowsAzure.Storage.Table; 8 | 9 | /// 10 | /// Favorite Distribution List data table entity class used to represent pinned distribution list records. 11 | /// 12 | public class FavoriteDistributionListTableEntity : TableEntity 13 | { 14 | /// 15 | /// Gets or sets a value indicating whether record is pinned or not. 16 | /// 17 | public bool PinStatus { get; set; } 18 | 19 | /// 20 | /// Gets or sets Row key with distribution list id. 21 | /// 22 | [IgnoreProperty] 23 | public string GroupId 24 | { 25 | get { return this.RowKey; } 26 | set { this.RowKey = value; } 27 | } 28 | 29 | /// 30 | /// Gets or sets partition key with user's object Id. 31 | /// 32 | [IgnoreProperty] 33 | public string UserObjectId 34 | { 35 | get { return this.PartitionKey; } 36 | set { this.PartitionKey = value; } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/PeoplePresenceData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | using Microsoft.Teams.Apps.DLLookup.Constants; 8 | 9 | /// 10 | /// PeoplePresenceData model is for member's presence information. 11 | /// 12 | public class PeoplePresenceData 13 | { 14 | /// 15 | /// Gets or sets the member's availability. 16 | /// Refer the list of base presence information https://docs.microsoft.com/en-us/graph/api/resources/presence?view=graph-rest-beta#properties 17 | /// Possible values are Available, AvailableIdle, Away, BeRightBack, Busy, BusyIdle, DoNotDisturb, Offline, PresenceUnknown 18 | /// 19 | public string Availability { get; set; } 20 | 21 | /// 22 | /// Gets value of sort order based on availability. 23 | /// 24 | public int AvailabilitySortOrder 25 | { 26 | get 27 | { 28 | int sortOrder = 6; 29 | switch (!string.IsNullOrEmpty(this.Availability) ? this.Availability : string.Empty) 30 | { 31 | case PresenceStates.Available: 32 | sortOrder = 0; 33 | break; 34 | case PresenceStates.Busy: 35 | sortOrder = 1; 36 | break; 37 | case PresenceStates.DoNotDisturb: 38 | sortOrder = 2; 39 | break; 40 | case PresenceStates.BeRightBack: 41 | sortOrder = 3; 42 | break; 43 | case PresenceStates.Away: 44 | sortOrder = 4; 45 | break; 46 | case PresenceStates.Offline: 47 | sortOrder = 5; 48 | break; 49 | } 50 | 51 | return sortOrder; 52 | } 53 | } 54 | 55 | /// 56 | /// Gets or sets member's user principal name as registered in Azure AD. 57 | /// 58 | public string UserPrincipalName { get; set; } 59 | 60 | /// 61 | /// Gets or sets user object id as registered in Azure AD. 62 | /// 63 | // [JsonProperty("Id")] 64 | public string Id { get; set; } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/StorageOptions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | /// 8 | /// This class contain value application configuration properties for Microsoft Azure Table storage. 9 | /// 10 | public class StorageOptions 11 | { 12 | /// 13 | /// Gets or sets connection string. 14 | /// 15 | public string ConnectionString { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/UserPageSizeChoice.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Models 6 | { 7 | /// 8 | /// This enumeration represents different pages of application. 9 | /// 10 | public enum PageType 11 | { 12 | /// 13 | /// Help page. 14 | /// 15 | HelpPage, 16 | 17 | /// 18 | /// Distribution List page. 19 | /// 20 | DistributionList, 21 | 22 | /// 23 | /// Distribution List Members page. 24 | /// 25 | DistributionListMembers, 26 | } 27 | 28 | /// 29 | /// This model represents User's page size choice. 30 | /// 31 | public class UserPageSizeChoice 32 | { 33 | /// 34 | /// Gets or sets user choice for page size for a page. 35 | /// 36 | public int PageSize { get; set; } 37 | 38 | /// 39 | /// Gets or sets to which page the users choice for page size belongs to. 40 | /// 41 | public PageType PageId { get; set; } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Models/UserPageSizeChoiceTableEntity.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using Microsoft.WindowsAzure.Storage.Table; 8 | 9 | /// 10 | /// User page size choice table entity class used to represent user's page size choices. 11 | /// 12 | public class UserPageSizeChoiceTableEntity : TableEntity 13 | { 14 | /// 15 | /// Gets or sets distribution list page size. 16 | /// 17 | public int DistributionListPageSize { get; set; } 18 | 19 | /// 20 | /// Gets or sets distribution list members page size. 21 | /// 22 | public int DistributionListMemberPageSize { get; set; } 23 | 24 | /// 25 | /// Gets or sets Partition key with "default" value. 26 | /// 27 | [IgnoreProperty] 28 | public string DefaultValue 29 | { 30 | get { return this.PartitionKey; } 31 | set { this.PartitionKey = value; } 32 | } 33 | 34 | /// 35 | /// Gets or sets Row key with user's AAD object Id. 36 | /// 37 | [IgnoreProperty] 38 | public string UserObjectId 39 | { 40 | get { return this.RowKey; } 41 | set { this.RowKey = value; } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup 6 | { 7 | using System; 8 | using Microsoft.AspNetCore; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.Logging; 12 | 13 | /// 14 | /// Default Program class. 15 | /// 16 | public class Program 17 | { 18 | /// 19 | /// Default Main method. 20 | /// 21 | /// string array input parameters. 22 | public static void Main(string[] args) 23 | { 24 | CreateWebHostBuilder(args).Build().Run(); 25 | } 26 | 27 | /// 28 | /// Method to create default builder. 29 | /// 30 | /// string input parameter from Main method. 31 | /// Calls Startup method. 32 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 33 | WebHost.CreateDefaultBuilder(args) 34 | .ConfigureAppConfiguration((hostingContext, config) => 35 | { 36 | config.AddEnvironmentVariables(); 37 | }) 38 | .UseStartup() 39 | .ConfigureLogging((hostingContext, logging) => 40 | { 41 | // hostingContext.HostingEnvironment can be used to determine environments as well. 42 | var appInsightKey = hostingContext.Configuration["ApplicationInsights:InstrumentationKey"]; 43 | logging.AddApplicationInsights(appInsightKey); 44 | 45 | // This will capture Info level traces and above. 46 | if (!Enum.TryParse(hostingContext.Configuration["ApplicationInsights:LogLevel:Default"], out LogLevel logLevel)) 47 | { 48 | logLevel = LogLevel.Information; 49 | } 50 | 51 | logging.AddFilter(string.Empty, logLevel); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/BaseStorageProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Options; 10 | using Microsoft.Teams.Apps.DLLookup.Models; 11 | using Microsoft.WindowsAzure.Storage; 12 | using Microsoft.WindowsAzure.Storage.RetryPolicies; 13 | using Microsoft.WindowsAzure.Storage.Table; 14 | 15 | /// 16 | /// Implements storage provider which initializes table if not exists and provide table client instance. 17 | /// 18 | public class BaseStorageProvider 19 | { 20 | /// 21 | /// Microsoft Azure Table storage connection string. 22 | /// 23 | private readonly string connectionString; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// A set of key/value application storage configuration properties. 29 | /// Table name of azure table storage to initialize. 30 | public BaseStorageProvider(IOptionsMonitor storageOptions, string tableName) 31 | { 32 | storageOptions = storageOptions ?? throw new ArgumentNullException(nameof(storageOptions)); 33 | this.connectionString = storageOptions.CurrentValue.ConnectionString ?? throw new ArgumentNullException(nameof(storageOptions)); 34 | this.TableName = tableName; 35 | this.InitializeTask = new Lazy(() => this.InitializeAsync()); 36 | } 37 | 38 | /// 39 | /// Gets or sets task for initialization. 40 | /// 41 | protected Lazy InitializeTask { get; set; } 42 | 43 | /// 44 | /// Gets or sets Microsoft Azure Table storage table name. 45 | /// 46 | protected string TableName { get; set; } 47 | 48 | /// 49 | /// Gets or sets a table in the Microsoft Azure Table storage. 50 | /// 51 | protected CloudTable DlLookupCloudTable { get; set; } 52 | 53 | /// 54 | /// Ensures Microsoft Azure Table Storage should be created before working on table. 55 | /// 56 | /// Represents an asynchronous operation. 57 | protected async Task EnsureInitializedAsync() 58 | { 59 | await this.InitializeTask.Value; 60 | } 61 | 62 | /// 63 | /// Create storage table if it does not exist. 64 | /// 65 | /// representing the asynchronous operation task which represents table is created if it does not exists. 66 | private async Task InitializeAsync() 67 | { 68 | // Exponential retry policy with back off of 1 seconds and 3 retries. 69 | var exponentialRetryPolicy = new TableRequestOptions() 70 | { 71 | RetryPolicy = new ExponentialRetry(TimeSpan.FromSeconds(1), 3), 72 | }; 73 | 74 | CloudStorageAccount storageAccount = CloudStorageAccount.Parse(this.connectionString); 75 | CloudTableClient cloudTableClient = storageAccount.CreateCloudTableClient(); 76 | cloudTableClient.DefaultRequestOptions = exponentialRetryPolicy; 77 | this.DlLookupCloudTable = cloudTableClient.GetTableReference(this.TableName); 78 | await this.DlLookupCloudTable.CreateIfNotExistsAsync(); 79 | 80 | return this.DlLookupCloudTable; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/FavoriteDistributionListDataRepository.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Microsoft.Teams.Apps.DLLookup.Helpers; 14 | using Microsoft.Teams.Apps.DLLookup.Helpers.Extentions; 15 | using Microsoft.Teams.Apps.DLLookup.Models; 16 | using Microsoft.Teams.Apps.DLLookup.Repositories.Interfaces; 17 | 18 | /// 19 | /// This class contains read, write and update operations for distribution list member data on AAD and table storage. 20 | /// 21 | public class FavoriteDistributionListDataRepository : FavoriteDistributionListStorageProvider, IFavoriteDistributionListDataRepository 22 | { 23 | /// 24 | /// MS Graph batch limit is 20. Setting it 10 here as 2 APIs are added in batch. 25 | /// https://docs.microsoft.com/en-us/graph/known-issues#json-batching. 26 | /// 27 | private const int BatchSplitCount = 10; 28 | private readonly ILogger logger; 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// A set of key/value application configuration properties for Microsoft Azure Table storage. 34 | /// Instance to send logs to the Application Insights service. 35 | public FavoriteDistributionListDataRepository( 36 | IOptionsMonitor storageOptions, 37 | ILogger logger) 38 | : base(storageOptions, logger) 39 | { 40 | this.logger = logger; 41 | } 42 | 43 | /// 44 | /// Creates/Updates favorite distribution list data in table storage. 45 | /// 46 | /// Instance of favoriteDistributionListData. 47 | /// Returns data model. 48 | public async Task CreateOrUpdateFavoriteDistributionListAsync( 49 | FavoriteDistributionListData favoriteDistributionListData) 50 | { 51 | FavoriteDistributionListTableEntity favoriteDistributionListDataEntity = new FavoriteDistributionListTableEntity() 52 | { 53 | GroupId = favoriteDistributionListData.Id, 54 | PinStatus = favoriteDistributionListData.IsPinned, 55 | UserObjectId = favoriteDistributionListData.UserObjectId, 56 | }; 57 | 58 | await this.AddFavoriteDistributionListToStorageAsync(favoriteDistributionListDataEntity); 59 | } 60 | 61 | /// 62 | /// Gets distribution list data from MS Graph based on search query. 63 | /// 64 | /// Search query used to filter distribution list. 65 | /// Token to access MS graph. 66 | /// Distribution lists filtered with search query. 67 | public async Task> GetDistributionListsAsync( 68 | string query, string accessToken) 69 | { 70 | GraphUtilityHelper graphClient = new GraphUtilityHelper(accessToken); 71 | var distributionList = await graphClient.GetDistributionListsAsync(query, this.logger); 72 | return distributionList.ToList(); 73 | } 74 | 75 | /// 76 | /// Get favorite distribution list details and members count from Graph. 77 | /// 78 | /// List of Distribution List Ids. 79 | /// Token to access MS graph. 80 | /// Count of members in distribution list. 81 | public async Task> GetDistributionListDetailsFromGraphAsync( 82 | List groupIds, 83 | string accessToken) 84 | { 85 | // MS Graph batch limit is 20 86 | // refer https://docs.microsoft.com/en-us/graph/known-issues#json-batching to known issues with Microsoft Graph batch APIs 87 | IEnumerable> groupBatches = groupIds.SplitList(BatchSplitCount); 88 | List distributionListList = new List(); 89 | GraphUtilityHelper graphClient = new GraphUtilityHelper(accessToken); 90 | 91 | foreach (List groupBatch in groupBatches) 92 | { 93 | try 94 | { 95 | distributionListList.AddRange(await graphClient.GetDistributionListDetailsAsync(groupBatch, this.logger)); 96 | } 97 | catch (Exception ex) 98 | { 99 | this.logger.LogError(ex, $"An error occurred in GetDistributionListDetailsFromGraphAsync."); 100 | } 101 | } 102 | 103 | return distributionListList; 104 | } 105 | 106 | /// 107 | /// Gets favorite Distribution List details from Graph. 108 | /// 109 | /// Favorite Distribution List data from storage. 110 | /// Token to access MS graph. 111 | /// Favorite distribution list data from graph. 112 | public async Task> GetFavoriteDistributionListsFromGraphAsync( 113 | IEnumerable favoriteDistributionListEntities, 114 | string accessToken) 115 | { 116 | List favoriteDistributionList = new List(); 117 | 118 | List groupIds = favoriteDistributionListEntities.Select(dl => dl.GroupId).ToList(); 119 | List distributionList = await this.GetDistributionListDetailsFromGraphAsync(groupIds, accessToken); 120 | 121 | foreach (FavoriteDistributionListTableEntity currentItem in favoriteDistributionListEntities) 122 | { 123 | DistributionList currentDistributionList = distributionList.Find(dl => dl.Id == currentItem.GroupId); 124 | if (currentDistributionList == null) 125 | { 126 | continue; 127 | } 128 | 129 | favoriteDistributionList.Add( 130 | new FavoriteDistributionListData 131 | { 132 | IsPinned = currentItem.PinStatus, 133 | DisplayName = currentDistributionList.DisplayName, 134 | Mail = currentDistributionList.Mail, 135 | ContactsCount = currentDistributionList.MembersCount, 136 | Id = currentItem.GroupId, 137 | UserObjectId = currentItem.UserObjectId, 138 | }); 139 | } 140 | 141 | return favoriteDistributionList; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/FavoriteDistributionListMemberDataRepository.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Microsoft.Teams.Apps.DLLookup.Helpers; 14 | using Microsoft.Teams.Apps.DLLookup.Models; 15 | using Microsoft.Teams.Apps.DLLookup.Repositories.Interfaces; 16 | 17 | /// 18 | /// This class contains read and write operations for distribution list member on AAD and table storage. 19 | /// 20 | public class FavoriteDistributionListMemberDataRepository : FavoriteDistributionListMemberStorageProvider, IFavoriteDistributionListMemberDataRepository 21 | { 22 | private readonly ILogger logger; 23 | private GraphUtilityHelper graphClient; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// A set of key/value application configuration properties for Microsoft Azure Table storage. 29 | /// Instance to send logs to the Application Insights service. 30 | public FavoriteDistributionListMemberDataRepository( 31 | IOptionsMonitor storageOptions, 32 | ILogger logger) 33 | : base(storageOptions, logger) 34 | { 35 | this.logger = logger; 36 | } 37 | 38 | /// 39 | /// Adds distribution list member data. 40 | /// 41 | /// Favorite distribution list member data to be stored in database. 42 | /// A representing the result of the asynchronous operation. 43 | public async Task AddFavoriteDistributionListMemberAsync( 44 | FavoriteDistributionListMemberData favoriteDistributionListMemberData) 45 | { 46 | FavoriteDistributionListMemberTableEntity favoriteDistributionListMemberDataEntity = new FavoriteDistributionListMemberTableEntity() 47 | { 48 | DistributionListMemberId = (favoriteDistributionListMemberData.PinnedUserId + favoriteDistributionListMemberData.DistributionListId).ToLower(), 49 | DistributionListId = favoriteDistributionListMemberData.DistributionListId, 50 | UserObjectId = favoriteDistributionListMemberData.UserObjectId, 51 | }; 52 | 53 | await this.AddFavoriteMemberToStorageAsync(favoriteDistributionListMemberDataEntity); 54 | } 55 | 56 | /// 57 | /// Gets Distribution List members from Graph and table storage. 58 | /// 59 | /// Distribution list id to filter records. 60 | /// Token to access MS graph. 61 | /// User's Azure Active Directory Id. 62 | /// A collection of distribution list members. 63 | public async Task> GetMembersAsync( 64 | string groupId, 65 | string accessToken, 66 | string userObjectId) 67 | { 68 | this.graphClient = new GraphUtilityHelper(accessToken); 69 | 70 | List distributionListMemberList = await this.graphClient.GetDistributionListMembersAsync(groupId, this.logger); 71 | 72 | IEnumerable favoriteDistributionListMemberEntity = await this.GetFavoriteMembersFromStorageAsync(userObjectId); 73 | foreach (DistributionListMember member in distributionListMemberList) 74 | { 75 | string distributionListMemberId = member.UserObjectId + groupId; 76 | foreach (FavoriteDistributionListMemberTableEntity entity in favoriteDistributionListMemberEntity) 77 | { 78 | if (entity.DistributionListMemberId == distributionListMemberId) 79 | { 80 | member.IsPinned = true; 81 | } 82 | } 83 | } 84 | 85 | return distributionListMemberList 86 | .Where(member => member.Type == "#microsoft.graph.group" 87 | || string.Equals(member.UserType, "member", StringComparison.OrdinalIgnoreCase)) 88 | .ToList(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/FavoriteDistributionListMemberStorageProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Microsoft.Teams.Apps.DLLookup.Models; 14 | using Microsoft.WindowsAzure.Storage.Table; 15 | 16 | /// 17 | /// The class contains read, write and delete operations for distribution list member on table storage. 18 | /// 19 | public class FavoriteDistributionListMemberStorageProvider : BaseStorageProvider 20 | { 21 | private const string FavoriteMembersTableName = "FavoriteDistributionListMembers"; 22 | 23 | /// 24 | /// Instance to send logs to the Application Insights service. 25 | /// 26 | private readonly ILogger logger; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// A set of key/value application configuration properties for Microsoft Azure Table storage. 32 | /// Sends logs to the Application Insights service. 33 | public FavoriteDistributionListMemberStorageProvider( 34 | IOptionsMonitor storageOptions, 35 | ILogger logger) 36 | : base(storageOptions, FavoriteMembersTableName) 37 | { 38 | this.logger = logger; 39 | } 40 | 41 | /// 42 | /// Adds favorite distribution list member data to table storage. 43 | /// 44 | /// Favorite distribution list member data to be added to storage. 45 | /// A task that represents the work queued to execute. 46 | public async Task AddFavoriteMemberToStorageAsync(FavoriteDistributionListMemberTableEntity favoriteDistributionListMemberDataEntity) 47 | { 48 | try 49 | { 50 | await this.EnsureInitializedAsync(); 51 | TableOperation operation = TableOperation.InsertOrReplace(favoriteDistributionListMemberDataEntity); 52 | await this.DlLookupCloudTable.ExecuteAsync(operation); 53 | } 54 | catch (Exception ex) 55 | { 56 | this.logger.LogError(ex, $"An error occurred in AddFavoriteMemberToStorageAsync: DistributionListMemberId: {favoriteDistributionListMemberDataEntity.UserObjectId}."); 57 | throw; 58 | } 59 | } 60 | 61 | /// 62 | /// Gets favorite distribution list members from table storage. 63 | /// 64 | /// User's Azure Active Directory Id. 65 | /// List of pinned members. 66 | public async Task> GetFavoriteMembersFromStorageAsync(string userObjectId) 67 | { 68 | try 69 | { 70 | await this.EnsureInitializedAsync(); 71 | string partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, userObjectId); 72 | TableQuery query = new TableQuery().Where(partitionKeyFilter); 73 | IList entities = await this.ExecuteQueryAsync(query); 74 | return entities; 75 | } 76 | catch (Exception ex) 77 | { 78 | this.logger.LogError(ex, $"An error occurred in GetFavoriteMembersFromStorageAsync."); 79 | throw; 80 | } 81 | } 82 | 83 | /// 84 | /// Removes Distribution List member from table storage. 85 | /// 86 | /// Favorite distribution list member data to be deleted from storage. 87 | /// A task that represents the work queued to execute. 88 | public async Task DeleteFavoriteMemberFromStorageAsync(FavoriteDistributionListMemberTableEntity favoriteDistributionListMemberTableEntity) 89 | { 90 | try 91 | { 92 | await this.EnsureInitializedAsync(); 93 | TableOperation operation = TableOperation.Delete(favoriteDistributionListMemberTableEntity); 94 | await this.DlLookupCloudTable.ExecuteAsync(operation); 95 | } 96 | catch (Exception ex) 97 | { 98 | this.logger.LogError(ex, $"An error occurred in DeleteFavoriteMemberFromStorageAsync: UserObjectId: {favoriteDistributionListMemberTableEntity.UserObjectId}."); 99 | throw; 100 | } 101 | } 102 | 103 | /// 104 | /// Gets a favorite distribution list member from table storage. 105 | /// 106 | /// Pinned member id and distribution id as row key. 107 | /// User's Azure Active Directory Id. 108 | /// Favorite distribution list member record. 109 | public async Task GetFavoriteMemberFromStorageAsync(string pinnedDistributionListId, string userObjectId) 110 | { 111 | try 112 | { 113 | await this.EnsureInitializedAsync(); 114 | TableOperation operation = TableOperation.Retrieve(userObjectId, pinnedDistributionListId.ToLower()); 115 | TableResult result = await this.DlLookupCloudTable.ExecuteAsync(operation); 116 | return result.Result as FavoriteDistributionListMemberTableEntity; 117 | } 118 | catch (Exception ex) 119 | { 120 | this.logger.LogError(ex, $"An error occurred in GetFavoriteDistributionListFromStorageAsync: userObjectId: {userObjectId}."); 121 | throw; 122 | } 123 | } 124 | 125 | /// 126 | /// Execute Table query operation. 127 | /// 128 | /// Search query used to filter distribution list. 129 | /// Optional parameter. Maximum number of desired entities. 130 | /// Cancellation token details. 131 | /// Result of the asynchronous operation. 132 | private async Task> ExecuteQueryAsync( 133 | TableQuery query, 134 | int? count = null, 135 | CancellationToken cancellationToken = default) 136 | { 137 | query.TakeCount = count; 138 | 139 | try 140 | { 141 | List result = new List(); 142 | TableContinuationToken token = null; 143 | 144 | do 145 | { 146 | TableQuerySegment segment = await this.DlLookupCloudTable.ExecuteQuerySegmentedAsync(query, token); 147 | token = segment.ContinuationToken; 148 | result.AddRange(segment); 149 | } 150 | while (token != null 151 | && !cancellationToken.IsCancellationRequested 152 | && (count == null || result.Count < count.Value)); 153 | 154 | return result; 155 | } 156 | catch (Exception ex) 157 | { 158 | this.logger.LogError(ex, "Error occurred while executing the table query."); 159 | throw; 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/FavoriteDistributionListStorageProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Options; 12 | using Microsoft.Teams.Apps.DLLookup.Models; 13 | using Microsoft.WindowsAzure.Storage.Table; 14 | 15 | /// 16 | /// The class contains read, create and delete operations for distribution list on table storage. 17 | /// 18 | public class FavoriteDistributionListStorageProvider : BaseStorageProvider 19 | { 20 | private const string FavoriteDistributionListsTableName = "FavoriteDistributionLists"; 21 | 22 | /// 23 | /// Instance to send logs to the Application Insights service. 24 | /// 25 | private readonly ILogger logger; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// A set of key/value application configuration properties for Microsoft Azure Table storage. 31 | /// Sends logs to the Application Insights service. 32 | public FavoriteDistributionListStorageProvider( 33 | IOptionsMonitor storageOptions, 34 | ILogger logger) 35 | : base(storageOptions, FavoriteDistributionListsTableName) 36 | { 37 | this.logger = logger; 38 | } 39 | 40 | /// 41 | /// Gets all favorite Distribution Lists from table storage. 42 | /// 43 | /// User's Azure Active Directory Id. 44 | /// List of favorite Distribution List entities. 45 | public async Task> GetFavoriteDistributionListsFromStorageAsync(string userObjectId) 46 | { 47 | try 48 | { 49 | await this.EnsureInitializedAsync(); 50 | string filter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, userObjectId); 51 | var query = new TableQuery().Where(filter); 52 | var result = await this.DlLookupCloudTable.ExecuteQuerySegmentedAsync(query, null); 53 | return result; 54 | } 55 | catch (Exception ex) 56 | { 57 | this.logger.LogError(ex, $"An error occurred in GetFavoriteDistributionListsFromStorageAsync."); 58 | throw; 59 | } 60 | } 61 | 62 | /// 63 | /// Adds favorite distribution list to storage. 64 | /// 65 | /// Distribution list entity to be added as favorite. 66 | /// Add operation response. 67 | public async Task AddFavoriteDistributionListToStorageAsync(FavoriteDistributionListTableEntity favoriteDistributionListDataEntity) 68 | { 69 | try 70 | { 71 | await this.EnsureInitializedAsync(); 72 | TableOperation operation = TableOperation.InsertOrReplace(favoriteDistributionListDataEntity); 73 | await this.DlLookupCloudTable.ExecuteAsync(operation); 74 | } 75 | catch (Exception ex) 76 | { 77 | this.logger.LogError(ex, $"An error occurred in AddFavoriteDistributionListToStorageAsync: UserObjectId: {favoriteDistributionListDataEntity.UserObjectId}."); 78 | throw; 79 | } 80 | } 81 | 82 | /// 83 | /// Delete an entity in the table storage. 84 | /// 85 | /// Distribution list entity to be removed as favorite. 86 | /// A delete task that represents the work queued to execute. 87 | public async Task RemoveFavoriteDistributionListFromStorageAsync(FavoriteDistributionListTableEntity favoriteDistributionListEntity) 88 | { 89 | try 90 | { 91 | await this.EnsureInitializedAsync(); 92 | TableOperation operation = TableOperation.Delete(favoriteDistributionListEntity); 93 | await this.DlLookupCloudTable.ExecuteAsync(operation); 94 | } 95 | catch (Exception ex) 96 | { 97 | this.logger.LogError(ex, $"An error occurred in RemoveFavoriteDistributionListFromStorageAsync: UserObjectId: {favoriteDistributionListEntity.UserObjectId}."); 98 | throw; 99 | } 100 | } 101 | 102 | /// 103 | /// Get an entity by the keys in the table storage. 104 | /// 105 | /// Distribution list Id to be deleted. 106 | /// User's Azure Active Directory Id. 107 | /// The entity matching the keys. 108 | public async Task GetFavoriteDistributionListFromStorageAsync(string favoriteDistributionListDataId, string userObjectId) 109 | { 110 | try 111 | { 112 | await this.EnsureInitializedAsync(); 113 | TableOperation operation = TableOperation.Retrieve(userObjectId, favoriteDistributionListDataId.ToLower()); 114 | TableResult result = await this.DlLookupCloudTable.ExecuteAsync(operation); 115 | return result.Result as FavoriteDistributionListTableEntity; 116 | } 117 | catch (Exception ex) 118 | { 119 | this.logger.LogError(ex, $"An error occurred in GetFavoriteDistributionListFromStorageAsync favoriteDistributionListDataId: {favoriteDistributionListDataId}."); 120 | throw; 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/Interfaces/IFavoriteDistributionListDataRepository.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories.Interfaces 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Teams.Apps.DLLookup.Models; 10 | 11 | /// 12 | /// This interface contains read/write operations for distribution list member data. 13 | /// 14 | public interface IFavoriteDistributionListDataRepository 15 | { 16 | /// 17 | /// Create or update user favorite distribution list data in storage. 18 | /// 19 | /// Instance of favoriteDistributionListData. 20 | /// A task that represents the work queued to execute operation. 21 | Task CreateOrUpdateFavoriteDistributionListAsync(FavoriteDistributionListData favoriteDistributionList); 22 | 23 | /// 24 | /// Get collection of distribution list data from MS Graph based on search query. 25 | /// 26 | /// Search query string to filter distribution list based on name. 27 | /// Token to access MS graph. 28 | /// A collection of filtered distribution lists based on query. 29 | Task> GetDistributionListsAsync(string query, string accessToken); 30 | 31 | /// 32 | /// Get collection of user favorite distribution lists from storage. 33 | /// 34 | /// User's Azure AD id. 35 | /// A collection of favorite distribution list entities. 36 | Task> GetFavoriteDistributionListsFromStorageAsync(string userObjectId); 37 | 38 | /// 39 | /// Adds favorite distribution list to storage. 40 | /// 41 | /// Distribution list entity to be added as favorite. 42 | /// An add task that represents the work queued to execute. 43 | Task AddFavoriteDistributionListToStorageAsync(FavoriteDistributionListTableEntity favoriteDistributionListEntity); 44 | 45 | /// 46 | /// Remove favorite distribution list from storage. 47 | /// 48 | /// Distribution list entity to be removed as favorite. 49 | /// A delete task that represents the work queued to execute. 50 | Task RemoveFavoriteDistributionListFromStorageAsync(FavoriteDistributionListTableEntity favoriteDistributionListEntity); 51 | 52 | /// 53 | /// Get user favorite distribution list from storage for user id. 54 | /// 55 | /// Distribution list id to be deleted. 56 | /// User Azure AD id. 57 | /// User favorite distribution list record. 58 | Task GetFavoriteDistributionListFromStorageAsync(string favoriteDistributionListId, string userObjectId); 59 | 60 | /// 61 | /// Gets favorite distribution list details using MS graph. 62 | /// 63 | /// List of favorite distribution list records. 64 | /// Token to access MS graph. 65 | /// A collection of favorite distribution list entities. 66 | Task> GetFavoriteDistributionListsFromGraphAsync( 67 | IEnumerable favoriteDistributionListEntities, 68 | string accessToken); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/Interfaces/IFavoriteDistributionListMemberDataRepository.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories.Interfaces 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Teams.Apps.DLLookup.Models; 10 | 11 | /// 12 | /// This interface contains read and write operations for distribution list member. 13 | /// 14 | public interface IFavoriteDistributionListMemberDataRepository 15 | { 16 | /// 17 | /// Add favorite distribution list member data to storage. 18 | /// 19 | /// Favorite distribution list member data to be stored in table storage. 20 | /// A representing the result of the asynchronous operation. 21 | Task AddFavoriteDistributionListMemberAsync(FavoriteDistributionListMemberData favoriteDistributionListMemberData); 22 | 23 | /// 24 | /// Get list of distribution list members based on group id and user id. 25 | /// 26 | /// Distribution list id to filter records. 27 | /// Token to access MS graph 28 | /// User's Azure AD Id. 29 | /// A collection of distribution lists. 30 | Task> GetMembersAsync(string groupId, string accessToken, string userObjectId); 31 | 32 | /// 33 | /// Adds user favorite distribution list member to storage. 34 | /// 35 | /// Favorite distribution list member data to be added to storage. 36 | /// A task that represents the work queued to execute. 37 | Task AddFavoriteMemberToStorageAsync(FavoriteDistributionListMemberTableEntity favoriteDistributionListMemberDataEntity); 38 | 39 | /// 40 | /// Get favorite distribution list members from storage. 41 | /// 42 | /// User's Azure AD id. 43 | /// Favorite Distribution List members from storage. 44 | Task> GetFavoriteMembersFromStorageAsync(string userObjectId); 45 | 46 | /// 47 | /// Removes distribution list member from storage. 48 | /// 49 | /// Favorite distribution list member data to be deleted from storage. 50 | /// A task that represents the work queued to execute. 51 | Task DeleteFavoriteMemberFromStorageAsync(FavoriteDistributionListMemberTableEntity favoriteDistributionListMemberTableEntity); 52 | 53 | /// 54 | /// Get user favorite distribution list member from storage. 55 | /// 56 | /// Unique member id. 57 | /// User's Azure AD id. 58 | /// User favorite distribution list record based on pinned member and group id. 59 | Task GetFavoriteMemberFromStorageAsync(string pinnedDistributionListId, string userObjectId); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/Interfaces/IPresenceDataRepository.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Teams.Apps.DLLookup.Models; 10 | 11 | /// 12 | /// Interface helps fetching user presence and contact data. 13 | /// 14 | public interface IPresenceDataRepository 15 | { 16 | /// 17 | /// Get User presence details executing in batches. 18 | /// 19 | /// Collection of people presence data object used to get presence information. 20 | /// Token to access MS graph. 21 | /// A collection of people presence data providing user presence information. 22 | Task> GetBatchUserPresenceAsync(PeoplePresenceData[] peoplePresenceData, string accessToken); 23 | 24 | /// 25 | /// Gets online members count in a distribution list. 26 | /// 27 | /// Distribution list id. 28 | /// Token to access MS graph. 29 | /// Online members count in given distribution list id. 30 | Task GetDistributionListMembersOnlineCountAsync(string groupId, string accessToken); 31 | } 32 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/PresenceDataRepository.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | extern alias BetaLib; 8 | 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Threading.Tasks; 12 | using Microsoft.Extensions.Caching.Memory; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Options; 15 | using Microsoft.Teams.Apps.DLLookup.Constants; 16 | using Microsoft.Teams.Apps.DLLookup.Helpers; 17 | using Microsoft.Teams.Apps.DLLookup.Helpers.Extentions; 18 | using Microsoft.Teams.Apps.DLLookup.Models; 19 | 20 | /// 21 | /// This class helps fetching user presence and contact data using MS Graph. 22 | /// 23 | public class PresenceDataRepository : IPresenceDataRepository 24 | { 25 | /// 26 | /// MS Graph batch limit is 20 27 | /// https://docs.microsoft.com/en-us/graph/known-issues#json-batching. 28 | /// 29 | private const int BatchSplitCount = 20; 30 | 31 | // Refer https://docs.microsoft.com/en-us/microsoftteams/presence-admins#presence-states-in-teams to learn more about Presence states in Teams 32 | private readonly List onlinePresenceOptions = new List { PresenceStates.Busy, PresenceStates.DoNotDisturb, PresenceStates.Available }; 33 | 34 | private readonly IMemoryCache memoryCache; 35 | private readonly ILogger logger; 36 | private readonly IOptions cacheOptions; 37 | 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | /// Singleton memory cache object. 42 | /// Singleton instance of cache configuration. 43 | /// Instance to send logs to the Application Insights service. 44 | public PresenceDataRepository(IMemoryCache memoryCache, IOptions cacheOptions, ILogger logger) 45 | { 46 | this.memoryCache = memoryCache; 47 | this.cacheOptions = cacheOptions; 48 | this.logger = logger; 49 | } 50 | 51 | /// 52 | /// Get User presence details in a batch. 53 | /// 54 | /// Array of People Presence Data object used to get presence information. 55 | /// Token to access MS graph. 56 | /// People Presence Data model data filled with presence information. 57 | public async Task> GetBatchUserPresenceAsync(PeoplePresenceData[] peoplePresenceData, string accessToken) 58 | { 59 | List peoplePresenceDataList = new List(); 60 | List peoplePresenceDataBatchResults = new List(); 61 | 62 | foreach (PeoplePresenceData member in peoplePresenceData) 63 | { 64 | string id = member.Id; 65 | if (!this.memoryCache.TryGetValue(id, out PeoplePresenceData peoplePresence)) 66 | { 67 | peoplePresence = new PeoplePresenceData() 68 | { 69 | UserPrincipalName = member.UserPrincipalName, 70 | Id = member.Id, 71 | }; 72 | peoplePresenceDataList.Add(peoplePresence); 73 | } 74 | else 75 | { 76 | peoplePresenceDataBatchResults.Add(peoplePresence); 77 | } 78 | } 79 | 80 | if (peoplePresenceDataList.Count > 0) 81 | { 82 | var presenceBatches = peoplePresenceDataList.SplitList(BatchSplitCount); 83 | GraphUtilityHelper graphClientBeta = new GraphUtilityHelper(accessToken); 84 | 85 | foreach (var presenceBatch in presenceBatches) 86 | { 87 | peoplePresenceDataBatchResults.AddRange(await graphClientBeta.GetUserPresenceAsync(presenceBatch, this.logger)); 88 | } 89 | } 90 | else 91 | { 92 | this.logger.LogInformation($"GetBatchUserPresenceAsync. Presence of all users found in memory."); 93 | } 94 | 95 | return peoplePresenceDataBatchResults; 96 | } 97 | 98 | /// 99 | /// Gets online members count in a distribution list. 100 | /// 101 | /// Distribution list id. 102 | /// Token to access MS graph. 103 | /// Online members count in distribution list. 104 | public async Task GetDistributionListMembersOnlineCountAsync(string groupId, string accessToken) 105 | { 106 | try 107 | { 108 | int onlineMembersCount = 0; 109 | GraphUtilityHelper graphClient = new GraphUtilityHelper(accessToken); 110 | var members = await this.GetMembersList(groupId, accessToken); 111 | 112 | var peoplePresenceDataList = new List(); 113 | 114 | foreach (DistributionListMember member in members) 115 | { 116 | string id = member.UserObjectId; 117 | if (!this.memoryCache.TryGetValue(id, out PeoplePresenceData peoplePresence)) 118 | { 119 | peoplePresence = new PeoplePresenceData() 120 | { 121 | UserPrincipalName = member.UserPrincipalName, 122 | Id = member.UserObjectId, 123 | }; 124 | peoplePresenceDataList.Add(peoplePresence); 125 | } 126 | else 127 | { 128 | if (this.onlinePresenceOptions.Contains(peoplePresence.Availability)) 129 | { 130 | onlineMembersCount++; 131 | } 132 | } 133 | } 134 | 135 | if (peoplePresenceDataList.Count > 0) 136 | { 137 | MemoryCacheEntryOptions options = new MemoryCacheEntryOptions 138 | { 139 | AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(this.cacheOptions.Value.CacheInterval), // cache will expire in 300 seconds or 5 minutes 140 | }; 141 | 142 | var presenceBatches = peoplePresenceDataList.SplitList(BatchSplitCount); 143 | 144 | foreach (var presenceBatch in presenceBatches) 145 | { 146 | List peoplePresenceResults = await graphClient.GetUserPresenceAsync(presenceBatch, this.logger); 147 | for (int i = 0; i < peoplePresenceResults.Count; i++) 148 | { 149 | this.memoryCache.Set(peoplePresenceResults[i].Id, peoplePresenceResults[i], options); 150 | if (this.onlinePresenceOptions.Contains(peoplePresenceResults[i].Availability)) 151 | { 152 | onlineMembersCount++; 153 | } 154 | } 155 | } 156 | } 157 | else 158 | { 159 | this.logger.LogInformation($"Presence of all users in group found in memory."); 160 | } 161 | 162 | return onlineMembersCount; 163 | } 164 | catch (Exception ex) 165 | { 166 | this.logger.LogError(ex, $"GetDistributionListMembersOnlineCountAsync. An error occurred: {ex.Message}"); 167 | throw; 168 | } 169 | } 170 | 171 | /// 172 | /// Gets distribution list members using group API. 173 | /// 174 | /// Distribution list id to get members list. 175 | /// Token to access MS graph. 176 | /// DistributionListMember data model. 177 | private async Task> GetMembersList(string groupId, string accessToken) 178 | { 179 | GraphUtilityHelper graphClient = new GraphUtilityHelper(accessToken); 180 | var dlMemberList = await graphClient.GetMembersListAsync(groupId, this.logger); 181 | return dlMemberList; 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/UserPageSizeChoiceDataRepository.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Options; 10 | using Microsoft.Teams.Apps.DLLookup.Models; 11 | 12 | /// 13 | /// This class helps to add and update page size for currently logged in user. 14 | /// 15 | public class UserPageSizeChoiceDataRepository : UserPageSizeStorageProvider 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// A set of key/value application configuration properties for Microsoft Azure Table storage. 21 | /// Sends logs to the Application Insights service. 22 | public UserPageSizeChoiceDataRepository( 23 | IOptionsMonitor storageOptions, 24 | ILogger logger) 25 | : base(storageOptions, logger) 26 | { 27 | } 28 | 29 | /// 30 | /// This method is used to store page size into database. 31 | /// 32 | /// Page size to be stored. 33 | /// Page for which the page size needs to be stored. 34 | /// User's Azure Active Directory Id. 35 | /// A representing the result of the asynchronous operation. 36 | public async Task CreateOrUpdateUserPageSizeChoiceDataAsync( 37 | int pageSize, 38 | PageType pageType, 39 | string userObjectId) 40 | { 41 | UserPageSizeChoiceTableEntity userPageSizeChoiceDataEntity = await this.GetUserPageSizeAsync("default", userObjectId); 42 | if (userPageSizeChoiceDataEntity == null) 43 | { 44 | userPageSizeChoiceDataEntity = new UserPageSizeChoiceTableEntity(); 45 | } 46 | 47 | userPageSizeChoiceDataEntity.DefaultValue = "default"; 48 | userPageSizeChoiceDataEntity.UserObjectId = userObjectId; 49 | if (pageType == PageType.DistributionList) 50 | { 51 | userPageSizeChoiceDataEntity.DistributionListPageSize = pageSize; 52 | } 53 | else 54 | { 55 | userPageSizeChoiceDataEntity.DistributionListMemberPageSize = pageSize; 56 | } 57 | 58 | await this.UpdateUserPageSizeAsync(userPageSizeChoiceDataEntity); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Repositories/UserPageSizeStorageProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup.Repositories 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.Teams.Apps.DLLookup.Models; 12 | using Microsoft.WindowsAzure.Storage.Table; 13 | 14 | /// 15 | /// This class helps to get, create and update page size for currently logged in user from storage. 16 | /// 17 | public class UserPageSizeStorageProvider : BaseStorageProvider 18 | { 19 | private const string UserPageSizeTableName = "UserPageSizeChoices"; 20 | 21 | /// 22 | /// Instance to send logs to the Application Insights service. 23 | /// 24 | private readonly ILogger logger; 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// A set of key/value application configuration properties for Microsoft Azure Table storage. 30 | /// Sends logs to the Application Insights service. 31 | public UserPageSizeStorageProvider( 32 | IOptionsMonitor storageOptions, 33 | ILogger logger) 34 | : base(storageOptions, UserPageSizeTableName) 35 | { 36 | this.logger = logger; 37 | } 38 | 39 | /// 40 | /// To query page size information of a particular user from table storage. 41 | /// 42 | /// User's Azure Active Directory Id. 43 | /// Distribution list and distribution list members page size. 44 | public async Task GetUserPageSizeChoice(string userObjectId) 45 | { 46 | try 47 | { 48 | await this.EnsureInitializedAsync(); 49 | TableOperation operation = TableOperation.Retrieve("default", userObjectId); 50 | TableResult result = await this.DlLookupCloudTable.ExecuteAsync(operation); 51 | return result.Result as UserPageSizeChoiceTableEntity; 52 | } 53 | catch (Exception ex) 54 | { 55 | this.logger.LogError(ex, $"An error occurred in GetUserPageSizeChoice: userObjectId: {userObjectId}."); 56 | throw; 57 | } 58 | } 59 | 60 | /// 61 | /// Get an entity by the keys in the table storage. 62 | /// 63 | /// The partition key of the entity. 64 | /// User's Azure Active Directory Id. 65 | /// The entity matching the keys. 66 | public async Task GetUserPageSizeAsync(string partitionKey, string userObjectId) 67 | { 68 | try 69 | { 70 | await this.EnsureInitializedAsync(); 71 | TableOperation operation = TableOperation.Retrieve(partitionKey.ToLower(), userObjectId.ToLower()); 72 | TableResult result = await this.DlLookupCloudTable.ExecuteAsync(operation); 73 | return result.Result as UserPageSizeChoiceTableEntity; 74 | } 75 | catch (Exception ex) 76 | { 77 | this.logger.LogError(ex, $"An error occurred in GetUserPageSizeAsync: userObjectId: {userObjectId}."); 78 | throw; 79 | } 80 | } 81 | 82 | /// 83 | /// Create or update an entity in the table storage. 84 | /// 85 | /// User page size entity to be updated. 86 | /// A task that represents the delete queued to execute. 87 | public async Task UpdateUserPageSizeAsync(UserPageSizeChoiceTableEntity userPageSizeChoiceTableEntity) 88 | { 89 | try 90 | { 91 | await this.EnsureInitializedAsync(); 92 | TableOperation operation = TableOperation.InsertOrReplace(userPageSizeChoiceTableEntity); 93 | await this.DlLookupCloudTable.ExecuteAsync(operation); 94 | } 95 | catch (Exception ex) 96 | { 97 | this.logger.LogError(ex, $"An error occurred in UpdateUserPageSizeAsync: UserObjectId: {userPageSizeChoiceTableEntity.UserObjectId}."); 98 | throw; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/Startup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.DLLookup 6 | { 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Identity.Client; 14 | using Microsoft.Teams.Apps.DLLookup.Authentication; 15 | using Microsoft.Teams.Apps.DLLookup.Helpers; 16 | using Microsoft.Teams.Apps.DLLookup.Helpers.Extentions; 17 | using Microsoft.Teams.Apps.DLLookup.Models; 18 | 19 | /// 20 | /// Default Startup class. 21 | /// 22 | public class Startup 23 | { 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// Instance of application configuration properties. 28 | public Startup(IConfiguration configuration) 29 | { 30 | this.Configuration = configuration; 31 | } 32 | 33 | /// 34 | /// Gets application configuration value. 35 | /// 36 | public IConfiguration Configuration { get; } 37 | 38 | /// 39 | /// This method gets called by the runtime. Use this method to add services to the container. 40 | /// 41 | /// IServiceCollection instance. 42 | public void ConfigureServices(IServiceCollection services) 43 | { 44 | var scopes = this.Configuration["AzureAd:GraphScope"].Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); 45 | IConfidentialClientApplication confidentialClientApp = ConfidentialClientApplicationBuilder.Create(this.Configuration["AzureAd:ClientId"]) 46 | .WithClientSecret(this.Configuration["AzureAd:ClientSecret"]) 47 | .Build(); 48 | 49 | services.AddMemoryCache(); 50 | services.AddSingleton(confidentialClientApp); 51 | services.AddDLLookupAuthentication(this.Configuration); 52 | services.AddSingleton(); 53 | services.AddSession(); 54 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1).AddSessionStateTempDataProvider(); 55 | services.AddApplicationInsightsTelemetry(this.Configuration["ApplicationInsights:InstrumentationKey"]); 56 | services.Configure(options => 57 | { 58 | options.ConnectionString = this.Configuration.GetValue("Storage:ConnectionString"); 59 | }); 60 | 61 | services.Configure(options => 62 | { 63 | options.CacheInterval = this.Configuration.GetValue("CacheInterval"); 64 | }); 65 | 66 | services.Configure(options => 67 | { 68 | options.ClientId = this.Configuration.GetValue("AzureAd:ClientId"); 69 | options.ClientSecret = this.Configuration.GetValue("AzureAd:ClientSecret"); 70 | options.GraphScope = this.Configuration.GetValue("AzureAd:GraphScope"); 71 | options.TenantId = this.Configuration.GetValue("AzureAd:TenantId"); 72 | }); 73 | 74 | // In production, the React files will be served from this directory 75 | services.AddSpaStaticFiles(configuration => 76 | { 77 | configuration.RootPath = "ClientApp/build"; 78 | }); 79 | 80 | services.AddRepositories(); 81 | services.AddHttpClient(); 82 | } 83 | 84 | /// 85 | /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 86 | /// 87 | /// IApplicationBuilder instance. 88 | /// IHostingEnvironment instance. 89 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 90 | { 91 | app.UseSession(); 92 | 93 | if (env.IsDevelopment()) 94 | { 95 | app.UseDeveloperExceptionPage(); 96 | } 97 | else 98 | { 99 | app.UseExceptionHandler("/Error"); 100 | app.UseHsts(); 101 | } 102 | 103 | // app.UseHttpsRedirection(); 104 | app.UseAuthentication(); 105 | 106 | app.UseStaticFiles(); 107 | app.UseSpaStaticFiles(); 108 | 109 | app.UseMvc(routes => 110 | { 111 | routes.MapRoute( 112 | name: "default", 113 | template: "{controller}/{action=Index}/{id?}"); 114 | }); 115 | 116 | app.UseSpa(spa => 117 | { 118 | spa.Options.SourcePath = "ClientApp"; 119 | 120 | if (env.IsDevelopment()) 121 | { 122 | spa.UseReactDevelopmentServer(npmScript: "start"); 123 | } 124 | }); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.DLLookup/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AzureAd": { 3 | "Instance": "", 4 | "TenantId": "", 5 | "ClientId": "", 6 | "ClientSecret": "", 7 | "ApplicationIdURI": "", 8 | "ValidIssuers": "", 9 | "GraphScope": "" 10 | }, 11 | "Storage": { 12 | "ConnectionString": "" 13 | }, 14 | "CacheInterval": 60, 15 | "ApplicationInsights": { 16 | "InstrumentationKey": "", 17 | "LogLevel": { 18 | "Default": "Information" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | :: ---------------------- 4 | :: KUDU Deployment Script 5 | :: Version: 1.0.17 6 | :: ---------------------- 7 | 8 | :: Prerequisites 9 | :: ------------- 10 | 11 | :: Verify node.js installed 12 | where node 2>nul >nul 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. 15 | goto error 16 | ) 17 | 18 | :: Setup 19 | :: ----- 20 | 21 | setlocal enabledelayedexpansion 22 | 23 | SET ARTIFACTS=%~dp0%..\artifacts 24 | 25 | IF NOT DEFINED DEPLOYMENT_SOURCE ( 26 | SET DEPLOYMENT_SOURCE=%~dp0%. 27 | ) 28 | 29 | IF NOT DEFINED DEPLOYMENT_TARGET ( 30 | SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot 31 | ) 32 | 33 | IF NOT DEFINED NEXT_MANIFEST_PATH ( 34 | SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest 35 | 36 | IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( 37 | SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest 38 | ) 39 | ) 40 | 41 | IF NOT DEFINED KUDU_SYNC_CMD ( 42 | :: Install kudu sync 43 | echo Installing Kudu Sync 44 | call npm install kudusync -g --silent 45 | IF !ERRORLEVEL! NEQ 0 goto error 46 | 47 | :: Locally just running "kuduSync" would also work 48 | SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd 49 | ) 50 | IF NOT DEFINED DEPLOYMENT_TEMP ( 51 | SET DEPLOYMENT_TEMP=%temp%\___deployTemp%random% 52 | SET CLEAN_LOCAL_DEPLOYMENT_TEMP=true 53 | ) 54 | 55 | IF DEFINED CLEAN_LOCAL_DEPLOYMENT_TEMP ( 56 | IF EXIST "%DEPLOYMENT_TEMP%" rd /s /q "%DEPLOYMENT_TEMP%" 57 | mkdir "%DEPLOYMENT_TEMP%" 58 | ) 59 | 60 | IF DEFINED MSBUILD_PATH goto MsbuildPathDefined 61 | SET MSBUILD_PATH=%ProgramFiles(x86)%\MSBuild\14.0\Bin\MSBuild.exe 62 | :MsbuildPathDefined 63 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 64 | :: Deployment 65 | :: ---------- 66 | 67 | echo Handling ASP.NET Core Web Application deployment. 68 | 69 | :: 1. Restore nuget packages 70 | echo Restoring NuGet packages 71 | call :ExecuteCmd dotnet restore "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.DLLookup.sln" 72 | IF !ERRORLEVEL! NEQ 0 goto error 73 | 74 | :: 2. Restore npm packages 75 | echo Restoring npm packages (this can take several minutes) 76 | pushd "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.DLLookup\ClientApp" 77 | call :ExecuteCmd npm install --no-audit 78 | IF !ERRORLEVEL! NEQ 0 ( 79 | echo First attempt failed, retrying once 80 | call :ExecuteCmd npm install --no-audit 81 | ) 82 | popd 83 | IF !ERRORLEVEL! NEQ 0 goto error 84 | 85 | :: 3. Build the client app 86 | echo Building the client app (this can take several minutes) 87 | pushd "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.DLLookup\ClientApp" 88 | call :ExecuteCmd npm run build 89 | popd 90 | IF !ERRORLEVEL! NEQ 0 goto error 91 | 92 | :: 4. Build and publish 93 | echo Building the application 94 | call :ExecuteCmd dotnet publish "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.DLLookup\Microsoft.Teams.Apps.DLLookup.csproj" --output "%DEPLOYMENT_TEMP%" --configuration Release -property:KuduDeployment=1 95 | IF !ERRORLEVEL! NEQ 0 goto error 96 | 97 | :: 5. KuduSync 98 | call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" 99 | IF !ERRORLEVEL! NEQ 0 goto error 100 | 101 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 102 | goto end 103 | 104 | :: Execute command routine that will echo out when error 105 | :ExecuteCmd 106 | setlocal 107 | set _CMD_=%* 108 | call %_CMD_% 109 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 110 | exit /b %ERRORLEVEL% 111 | 112 | :error 113 | endlocal 114 | echo An error has occurred during web site deployment. 115 | call :exitSetErrorLevel 116 | call :exitFromFunction 2>nul 117 | 118 | :exitSetErrorLevel 119 | exit /b 1 120 | 121 | :exitFromFunction 122 | () 123 | 124 | :end 125 | endlocal 126 | echo Finished successfully. 127 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.1.515" 4 | } 5 | } --------------------------------------------------------------------------------