From 72f08fc9cb706a3d4afe4641fd20810ef4e6dc5d Mon Sep 17 00:00:00 2001 From: "leon.hoppe" Date: Sat, 15 Apr 2023 17:54:37 +0200 Subject: [PATCH] Login finished --- .dockerignore | 25 +++++++++++ BetterIServ.Backend/.gitignore | 2 + .../BetterIServ.Backend.csproj | 21 ++++++++++ .../Controllers/HelperController.cs | 42 +++++++++++++++++++ BetterIServ.Backend/Dockerfile | 20 +++++++++ BetterIServ.Backend/Program.cs | 29 +++++++++++++ .../Properties/launchSettings.json | 41 ++++++++++++++++++ .../appsettings.Development.json | 8 ++++ BetterIServ.Backend/appsettings.json | 9 ++++ BetterIServ.Mobile/package.json | 3 +- .../src/app/Api/iserv.service.ts | 41 ++++++++++++++++++ BetterIServ.Mobile/src/app/app.component.html | 20 ++++----- BetterIServ.Mobile/src/app/app.component.scss | 16 ++----- BetterIServ.Mobile/src/app/app.component.ts | 31 ++++++++++---- BetterIServ.Mobile/src/app/app.routes.ts | 11 +++-- .../src/app/entities/userdata.ts | 6 +++ .../src/app/folder/folder.page.ts | 20 --------- .../folder.page.html => home/home.page.html} | 6 +-- .../folder.page.scss => home/home.page.scss} | 2 +- BetterIServ.Mobile/src/app/home/home.page.ts | 19 +++++++++ .../src/app/login/login.page.html | 20 +++++++++ .../src/app/login/login.page.scss | 24 +++++++++++ .../src/app/login/login.page.ts | 40 ++++++++++++++++++ BetterIServ.Mobile/src/main.ts | 2 + BetterIServ.sln | 16 +++++++ 25 files changed, 412 insertions(+), 62 deletions(-) create mode 100644 .dockerignore create mode 100644 BetterIServ.Backend/.gitignore create mode 100644 BetterIServ.Backend/BetterIServ.Backend.csproj create mode 100644 BetterIServ.Backend/Controllers/HelperController.cs create mode 100644 BetterIServ.Backend/Dockerfile create mode 100644 BetterIServ.Backend/Program.cs create mode 100644 BetterIServ.Backend/Properties/launchSettings.json create mode 100644 BetterIServ.Backend/appsettings.Development.json create mode 100644 BetterIServ.Backend/appsettings.json create mode 100644 BetterIServ.Mobile/src/app/Api/iserv.service.ts create mode 100644 BetterIServ.Mobile/src/app/entities/userdata.ts delete mode 100644 BetterIServ.Mobile/src/app/folder/folder.page.ts rename BetterIServ.Mobile/src/app/{folder/folder.page.html => home/home.page.html} (76%) rename BetterIServ.Mobile/src/app/{folder/folder.page.scss => home/home.page.scss} (99%) create mode 100644 BetterIServ.Mobile/src/app/home/home.page.ts create mode 100644 BetterIServ.Mobile/src/app/login/login.page.html create mode 100644 BetterIServ.Mobile/src/app/login/login.page.scss create mode 100644 BetterIServ.Mobile/src/app/login/login.page.ts create mode 100644 BetterIServ.sln diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/BetterIServ.Backend/.gitignore b/BetterIServ.Backend/.gitignore new file mode 100644 index 0000000..fdc2306 --- /dev/null +++ b/BetterIServ.Backend/.gitignore @@ -0,0 +1,2 @@ +bin +obj diff --git a/BetterIServ.Backend/BetterIServ.Backend.csproj b/BetterIServ.Backend/BetterIServ.Backend.csproj new file mode 100644 index 0000000..5f8aeea --- /dev/null +++ b/BetterIServ.Backend/BetterIServ.Backend.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + Linux + + + + + + + + + + .dockerignore + + + + diff --git a/BetterIServ.Backend/Controllers/HelperController.cs b/BetterIServ.Backend/Controllers/HelperController.cs new file mode 100644 index 0000000..2fe2a62 --- /dev/null +++ b/BetterIServ.Backend/Controllers/HelperController.cs @@ -0,0 +1,42 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc; + +namespace BetterIServ.Backend.Controllers; + +[ApiController] +public class HelperController : Controller { + + [HttpPost("/login")] + public async Task> Login([FromForm] string email, [FromForm] string password) { + try { + using var client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, AllowAutoRedirect = false }) { Timeout = TimeSpan.FromSeconds(5) }; + + var split = email.Split("@"); + var username = split[0]; + var domain = split[1]; + + var form = new FormUrlEncodedContent(new[] { + new KeyValuePair("_username", username), + new KeyValuePair("_password", password) + }); + + var request = new HttpRequestMessage { + RequestUri = new Uri($"https://{domain}/iserv/auth/login"), + Method = HttpMethod.Post, + Content = form + }; + + var response = await client.SendAsync(request); + var header = response.Headers.GetValues("Set-Cookie").First(); + var part = header.Split(";")[0]; + + if (!part.Contains("IServAuthSession")) throw new Exception(); + + return Ok(part.Replace("IServAuthSession=", "")); + } + catch (Exception e) { + return Unauthorized(); + } + } + +} \ No newline at end of file diff --git a/BetterIServ.Backend/Dockerfile b/BetterIServ.Backend/Dockerfile new file mode 100644 index 0000000..474f196 --- /dev/null +++ b/BetterIServ.Backend/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["BetterIServ.Backend/BetterIServ.Backend.csproj", "BetterIServ.Backend/"] +RUN dotnet restore "BetterIServ.Backend/BetterIServ.Backend.csproj" +COPY . . +WORKDIR "/src/BetterIServ.Backend" +RUN dotnet build "BetterIServ.Backend.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "BetterIServ.Backend.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "BetterIServ.Backend.dll"] diff --git a/BetterIServ.Backend/Program.cs b/BetterIServ.Backend/Program.cs new file mode 100644 index 0000000..faff5c2 --- /dev/null +++ b/BetterIServ.Backend/Program.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Cors.Infrastructure; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) { + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors(options => { + options.WithOrigins("http://localhost:8100"); + options.AllowCredentials(); +}); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/BetterIServ.Backend/Properties/launchSettings.json b/BetterIServ.Backend/Properties/launchSettings.json new file mode 100644 index 0000000..6484c4b --- /dev/null +++ b/BetterIServ.Backend/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:49668", + "sslPort": 44383 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5273", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7147;http://localhost:5273", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BetterIServ.Backend/appsettings.Development.json b/BetterIServ.Backend/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/BetterIServ.Backend/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/BetterIServ.Backend/appsettings.json b/BetterIServ.Backend/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/BetterIServ.Backend/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/BetterIServ.Mobile/package.json b/BetterIServ.Mobile/package.json index e01c263..5be9bbc 100644 --- a/BetterIServ.Mobile/package.json +++ b/BetterIServ.Mobile/package.json @@ -9,7 +9,8 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "lint": "ng lint" + "lint": "ng lint", + "serve": "ionic serve" }, "private": true, "dependencies": { diff --git a/BetterIServ.Mobile/src/app/Api/iserv.service.ts b/BetterIServ.Mobile/src/app/Api/iserv.service.ts new file mode 100644 index 0000000..d6ba35f --- /dev/null +++ b/BetterIServ.Mobile/src/app/Api/iserv.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {Userdata} from "../entities/userdata"; +import {firstValueFrom} from "rxjs"; + +@Injectable({ + providedIn: 'root', +}) +export class IServService { + + public userdata?: Userdata; + private backend: string = "http://localhost:5273"; + + constructor(private client: HttpClient) { + const data = localStorage.getItem("userdata"); + if (data != null) { + this.userdata = JSON.parse(data); + } + } + + public async login(email: string, password: string): Promise { + const split = email.split('@'); + this.userdata = {}; + this.userdata.username = split[0]; + this.userdata.domain = split[1]; + this.userdata.password = password; + + const data = new FormData(); + data.append("email", email); + data.append("password", password); + + try { + this.userdata.token = await firstValueFrom(this.client.post(`${this.backend}/login`, data, {responseType: "text"})); + localStorage.setItem("userdata", JSON.stringify(this.userdata)); + return true; + }catch (error) { + return false; + } + } + +} diff --git a/BetterIServ.Mobile/src/app/app.component.html b/BetterIServ.Mobile/src/app/app.component.html index 3fd9ace..f39944d 100644 --- a/BetterIServ.Mobile/src/app/app.component.html +++ b/BetterIServ.Mobile/src/app/app.component.html @@ -2,9 +2,9 @@ - - Inbox - hi@ionicframework.com + + BetterIServ + {{iserv.userdata?.username}}@{{iserv.userdata?.domain}} @@ -12,15 +12,13 @@ {{ p.title }} - - - Labels - - - - {{ label }} - + + + + Ausloggen + + diff --git a/BetterIServ.Mobile/src/app/app.component.scss b/BetterIServ.Mobile/src/app/app.component.scss index 0aeb6fe..4015eb8 100644 --- a/BetterIServ.Mobile/src/app/app.component.scss +++ b/BetterIServ.Mobile/src/app/app.component.scss @@ -22,27 +22,17 @@ ion-menu.md ion-note { padding-left: 10px; } -ion-menu.md ion-list#inbox-list { +ion-menu.md ion-list { border-bottom: 1px solid var(--ion-color-step-150, #d7d8da); } -ion-menu.md ion-list#inbox-list ion-list-header { +ion-menu.md ion-list ion-list-header { font-size: 22px; font-weight: 600; min-height: 20px; } -ion-menu.md ion-list#labels-list ion-list-header { - font-size: 16px; - - margin-bottom: 18px; - - color: #757575; - - min-height: 26px; -} - ion-menu.md ion-item { --padding-start: 10px; --padding-end: 10px; @@ -116,4 +106,4 @@ ion-note { ion-item.selected { --color: var(--ion-color-primary); -} \ No newline at end of file +} diff --git a/BetterIServ.Mobile/src/app/app.component.ts b/BetterIServ.Mobile/src/app/app.component.ts index c90389c..9b775f1 100644 --- a/BetterIServ.Mobile/src/app/app.component.ts +++ b/BetterIServ.Mobile/src/app/app.component.ts @@ -1,7 +1,8 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { RouterLink, RouterLinkActive } from '@angular/router'; +import {Router, RouterLink, RouterLinkActive} from '@angular/router'; import { IonicModule } from '@ionic/angular'; +import {IServService} from "./Api/iserv.service"; @Component({ selector: 'app-root', templateUrl: 'app.component.html', @@ -10,14 +11,26 @@ import { IonicModule } from '@ionic/angular'; imports: [IonicModule, RouterLink, RouterLinkActive, CommonModule], }) export class AppComponent { + public appPages = [ - { title: 'Inbox', url: '/folder/inbox', icon: 'mail' }, - { title: 'Outbox', url: '/folder/outbox', icon: 'paper-plane' }, - { title: 'Favorites', url: '/folder/favorites', icon: 'heart' }, - { title: 'Archived', url: '/folder/archived', icon: 'archive' }, - { title: 'Trash', url: '/folder/trash', icon: 'trash' }, - { title: 'Spam', url: '/folder/spam', icon: 'warning' }, + { title: 'Übersicht', url: '/home', icon: 'home' }, + { title: 'E-Mail', url: '/email', icon: 'mail' }, + { title: 'Dateien', url: '/files', icon: 'folder' }, + { title: 'Aufgaben', url: '/tasks', icon: 'clipboard' }, + { title: 'Stundenplan', url: '/schedule', icon: 'grid' }, + { title: 'Vertretungsplan', url: '/substitution', icon: 'list' }, ]; - public labels = ['Family', 'Friends', 'Notes', 'Work', 'Travel', 'Reminders']; - constructor() {} + public email = "leon.hoppe@hgbp.de"; + + constructor(private router: Router, public iserv: IServService) { + if (localStorage.getItem("userdata") == null) { + this.router.navigate(["login"]); + } + } + + public logout() { + localStorage.removeItem("userdata"); + this.router.navigate(["login"]); + } + } diff --git a/BetterIServ.Mobile/src/app/app.routes.ts b/BetterIServ.Mobile/src/app/app.routes.ts index 8e4f1e9..4ef11e2 100644 --- a/BetterIServ.Mobile/src/app/app.routes.ts +++ b/BetterIServ.Mobile/src/app/app.routes.ts @@ -3,12 +3,15 @@ import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', - redirectTo: 'folder/inbox', + redirectTo: 'home', pathMatch: 'full', }, { - path: 'folder/:id', - loadComponent: () => - import('./folder/folder.page').then((m) => m.FolderPage), + path: 'home', + loadComponent: () => import('./home/home.page').then( m => m.HomePage) + }, + { + path: 'login', + loadComponent: () => import('./login/login.page').then( m => m.LoginPage) }, ]; diff --git a/BetterIServ.Mobile/src/app/entities/userdata.ts b/BetterIServ.Mobile/src/app/entities/userdata.ts new file mode 100644 index 0000000..3c908a4 --- /dev/null +++ b/BetterIServ.Mobile/src/app/entities/userdata.ts @@ -0,0 +1,6 @@ +export interface Userdata { + domain?: string, + username?: string, + password?: string, + token?: string +} diff --git a/BetterIServ.Mobile/src/app/folder/folder.page.ts b/BetterIServ.Mobile/src/app/folder/folder.page.ts deleted file mode 100644 index 02fc128..0000000 --- a/BetterIServ.Mobile/src/app/folder/folder.page.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, inject, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { IonicModule } from '@ionic/angular'; - -@Component({ - selector: 'app-folder', - templateUrl: './folder.page.html', - styleUrls: ['./folder.page.scss'], - standalone: true, - imports: [IonicModule], -}) -export class FolderPage implements OnInit { - public folder!: string; - private activatedRoute = inject(ActivatedRoute); - constructor() {} - - ngOnInit() { - this.folder = this.activatedRoute.snapshot.paramMap.get('id') as string; - } -} diff --git a/BetterIServ.Mobile/src/app/folder/folder.page.html b/BetterIServ.Mobile/src/app/home/home.page.html similarity index 76% rename from BetterIServ.Mobile/src/app/folder/folder.page.html rename to BetterIServ.Mobile/src/app/home/home.page.html index 39aaa6a..2efad0c 100644 --- a/BetterIServ.Mobile/src/app/folder/folder.page.html +++ b/BetterIServ.Mobile/src/app/home/home.page.html @@ -3,19 +3,19 @@ - {{ folder }} + Übersicht - {{ folder }} + Übersicht
- {{ folder }} + Übersicht

Explore UI Components

diff --git a/BetterIServ.Mobile/src/app/folder/folder.page.scss b/BetterIServ.Mobile/src/app/home/home.page.scss similarity index 99% rename from BetterIServ.Mobile/src/app/folder/folder.page.scss rename to BetterIServ.Mobile/src/app/home/home.page.scss index 3797ba5..cceaa75 100644 --- a/BetterIServ.Mobile/src/app/folder/folder.page.scss +++ b/BetterIServ.Mobile/src/app/home/home.page.scss @@ -25,4 +25,4 @@ ion-menu-button { #container a { text-decoration: none; -} \ No newline at end of file +} diff --git a/BetterIServ.Mobile/src/app/home/home.page.ts b/BetterIServ.Mobile/src/app/home/home.page.ts new file mode 100644 index 0000000..8b73ce8 --- /dev/null +++ b/BetterIServ.Mobile/src/app/home/home.page.ts @@ -0,0 +1,19 @@ +import {Component, OnInit} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; + +@Component({ + selector: 'app-home', + templateUrl: './home.page.html', + styleUrls: ['./home.page.scss'], + standalone: true, + imports: [IonicModule, CommonModule, FormsModule] +}) +export class HomePage implements OnInit { + + ngOnInit() { + + } + +} diff --git a/BetterIServ.Mobile/src/app/login/login.page.html b/BetterIServ.Mobile/src/app/login/login.page.html new file mode 100644 index 0000000..1f89b7b --- /dev/null +++ b/BetterIServ.Mobile/src/app/login/login.page.html @@ -0,0 +1,20 @@ + + + BetterIServ + + + + +
+ + + IServ Anmeldedaten + + + + + Einloggen + + +
+
diff --git a/BetterIServ.Mobile/src/app/login/login.page.scss b/BetterIServ.Mobile/src/app/login/login.page.scss new file mode 100644 index 0000000..30b6972 --- /dev/null +++ b/BetterIServ.Mobile/src/app/login/login.page.scss @@ -0,0 +1,24 @@ +.card-container { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + ion-card { + width: 90%; + + ion-card-content { + display: flex; + flex-direction: column; + + ion-input { + margin-block: 15px; + } + + ion-button { + flex-grow: 0; + margin-inline: auto; + } + } + } +} diff --git a/BetterIServ.Mobile/src/app/login/login.page.ts b/BetterIServ.Mobile/src/app/login/login.page.ts new file mode 100644 index 0000000..c59ae75 --- /dev/null +++ b/BetterIServ.Mobile/src/app/login/login.page.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import {AlertController, IonicModule} from '@ionic/angular'; +import {IServService} from "../Api/iserv.service"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'app-login', + templateUrl: './login.page.html', + styleUrls: ['./login.page.scss'], + standalone: true, + imports: [IonicModule, CommonModule, FormsModule] +}) +export class LoginPage implements OnInit { + + constructor(private iservApi: IServService, private router: Router, private alerts: AlertController) { } + + ngOnInit() { + } + + public async onLogin(email?: string, password?: string) { + if (email == undefined || password == undefined) return; + + if (await this.iservApi.login(email, password)) { + await this.router.navigate(['home']); + }else { + const alert = await this.alerts.create({ + header: "Fehler", + message: "Die angegebenen Logindaten sind nicht korrekt!", + buttons: ['Ok'] + }); + + await alert.present(); + } + + console.log(this.iservApi.userdata); + } + +} diff --git a/BetterIServ.Mobile/src/main.ts b/BetterIServ.Mobile/src/main.ts index 5732693..b87f4ea 100644 --- a/BetterIServ.Mobile/src/main.ts +++ b/BetterIServ.Mobile/src/main.ts @@ -6,6 +6,7 @@ import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { routes } from './app/app.routes'; import { AppComponent } from './app/app.component'; import { environment } from './environments/environment'; +import {HttpClientModule} from "@angular/common/http"; if (environment.production) { enableProdMode(); @@ -15,6 +16,7 @@ bootstrapApplication(AppComponent, { providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, importProvidersFrom(IonicModule.forRoot({})), + importProvidersFrom(HttpClientModule), provideRouter(routes), ], }); diff --git a/BetterIServ.sln b/BetterIServ.sln new file mode 100644 index 0000000..cf3a8ee --- /dev/null +++ b/BetterIServ.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterIServ.Backend", "BetterIServ.Backend\BetterIServ.Backend.csproj", "{B0FA03B9-0A4D-4A71-BF71-0CF374F108CE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B0FA03B9-0A4D-4A71-BF71-0CF374F108CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0FA03B9-0A4D-4A71-BF71-0CF374F108CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0FA03B9-0A4D-4A71-BF71-0CF374F108CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0FA03B9-0A4D-4A71-BF71-0CF374F108CE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal