sessionkeys entered

This commit is contained in:
Bastian Wagner
2024-08-25 17:54:16 +02:00
parent 328062eaa0
commit 095f33e16f
13 changed files with 294 additions and 51 deletions

View File

@@ -4,6 +4,7 @@ import { ClientService } from 'src/client/client.service';
import { FormDataRequest } from 'nestjs-form-data'; import { FormDataRequest } from 'nestjs-form-data';
import { Client } from 'src/model/client.entity'; import { Client } from 'src/model/client.entity';
import { CustomLogger } from 'src/core/custom.logger'; import { CustomLogger } from 'src/core/custom.logger';
import { CreateUserDto } from 'src/model/dto/create-user.dto';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
@@ -14,11 +15,8 @@ export class AuthController {
) {} ) {}
@Post('register') @Post('register')
async register( async register(@Body() userDto: CreateUserDto) {
@Body('username') username: string, const user = await this.usersService.createUser(userDto);
@Body('password') password: string,
) {
const user = await this.usersService.createUser(username, password);
return user; return user;
} }
@@ -37,6 +35,16 @@ export class AuthController {
return token; return token;
} }
@Post('login-with-session-id')
async loginWithCode(
@Body('code') code: string,
@Body('client_id') clientId: string,
) {
const token = await this.usersService.loginWithSessionKey(code, clientId);
this.logger.log(`User logged in with code on client ${clientId}`);
return token;
}
@Get() @Get()
async getClient( async getClient(
@Query('client_id') clientId: string, @Query('client_id') clientId: string,

View File

@@ -13,6 +13,7 @@ import {
AuthorizationCodeRepository, AuthorizationCodeRepository,
} from 'src/model/auth-code.entity'; } from 'src/model/auth-code.entity';
import { LoggerModule } from 'src/core/logger.module'; import { LoggerModule } from 'src/core/logger.module';
import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity';
@Module({ @Module({
providers: [ providers: [
@@ -21,6 +22,7 @@ import { LoggerModule } from 'src/core/logger.module';
UserRepository, UserRepository,
ClientRepository, ClientRepository,
AuthorizationCodeRepository, AuthorizationCodeRepository,
SessionKeyRepository,
], ],
controllers: [AuthController], controllers: [AuthController],
imports: [ imports: [
@@ -29,7 +31,13 @@ import { LoggerModule } from 'src/core/logger.module';
signOptions: { expiresIn: '15m' }, signOptions: { expiresIn: '15m' },
}), }),
NestjsFormDataModule, NestjsFormDataModule,
TypeOrmModule.forFeature([User, Client, RedirectUri, AuthorizationCode]), TypeOrmModule.forFeature([
User,
Client,
RedirectUri,
AuthorizationCode,
SessionKey,
]),
LoggerModule, LoggerModule,
], ],
}) })

View File

@@ -0,0 +1,7 @@
export interface CreateUserDto {
username: string;
password: string;
clientId: string;
firstName: string;
lastName: string;
}

View File

@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
ManyToOne,
DataSource,
Repository,
} from 'typeorm';
import { User } from './user.entity';
import { Injectable } from '@nestjs/common';
@Entity()
export class SessionKey {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => User, (user) => user.sessionKeys, { eager: true })
user: User;
}
@Injectable()
export class SessionKeyRepository extends Repository<SessionKey> {
constructor(dataSource: DataSource) {
super(SessionKey, dataSource.createEntityManager());
}
findById(id: string): Promise<SessionKey> {
return this.findOneBy({ id });
}
}

View File

@@ -1,8 +1,3 @@
export interface CreateUserDto {
username: string;
password: string;
}
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Exclude } from 'class-transformer'; import { Exclude } from 'class-transformer';
import { import {
@@ -15,13 +10,14 @@ import {
CreateDateColumn, CreateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { AuthorizationCode } from './auth-code.entity'; import { AuthorizationCode } from './auth-code.entity';
import { SessionKey } from './session-key.entity';
@Entity() @Entity()
export class User { export class User {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column() @Column({ unique: true })
username: string; username: string;
@Exclude() @Exclude()
@@ -41,8 +37,13 @@ export class User {
@Column({ default: true }) @Column({ default: true })
isActive: boolean; isActive: boolean;
@Exclude()
@OneToMany(() => AuthorizationCode, (code) => code.user) @OneToMany(() => AuthorizationCode, (code) => code.user)
authorizationCodes: AuthorizationCode[]; authorizationCodes: AuthorizationCode[];
@Exclude()
@OneToMany(() => SessionKey, (key) => key.user)
sessionKeys: SessionKey[];
} }
@Injectable() @Injectable()

View File

@@ -9,6 +9,9 @@ import {
} from 'src/model/auth-code.entity'; } from 'src/model/auth-code.entity';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { CustomLogger } from 'src/core/custom.logger'; import { CustomLogger } from 'src/core/custom.logger';
import { CreateUserDto } from 'src/model/dto/create-user.dto';
import { SessionKeyRepository } from 'src/model/session-key.entity';
import { Client } from 'src/model/client.entity';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
@@ -17,20 +20,27 @@ export class UsersService {
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private userRepo: UserRepository, private userRepo: UserRepository,
private tokenRepo: AuthorizationCodeRepository, private tokenRepo: AuthorizationCodeRepository,
private sessionRepo: SessionKeyRepository,
private logger: CustomLogger, private logger: CustomLogger,
) {} ) {}
async createUser(username: string, password: string): Promise<User> { async createUser(userDto: CreateUserDto): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(userDto.password, 10);
const user: User = { const user: User = {
id: uuidv4(), id: uuidv4(),
username, username: userDto.username,
password: hashedPassword, password: hashedPassword,
isActive: true, isActive: true,
authorizationCodes: [], authorizationCodes: [],
firstName: userDto.firstName,
lastName: userDto.lastName,
sessionKeys: [],
}; };
const u = this.userRepo.create(user); const u = this.userRepo.create(user);
const uu = await this.userRepo.save(u); const uu = await this.userRepo.save(u);
this.logger.log(
`User ${userDto.username} created for client ${userDto.clientId}`,
);
return uu; return uu;
} }
@@ -38,7 +48,7 @@ export class UsersService {
username: string, username: string,
password: string, password: string,
clientId: string, clientId: string,
): Promise<{ code: string }> { ): Promise<{ code: string; session_key: string }> {
const user = await this.userRepo.findByUsername(username); const user = await this.userRepo.findByUsername(username);
if (!user) { if (!user) {
this.logger.error(`User ${username} not found`); this.logger.error(`User ${username} not found`);
@@ -57,7 +67,17 @@ export class UsersService {
this.logger.error(`Client ${clientId} not found`); this.logger.error(`Client ${clientId} not found`);
throw new HttpException('Invalid client', 401); throw new HttpException('Invalid client', 401);
} }
const token = await this.createAuthToken(user, client);
const s = this.sessionRepo.create({
user,
});
const session = await this.sessionRepo.save(s);
return { code: token.code, session_key: session.id };
}
async createAuthToken(user: User, client: Client) {
const token: AuthorizationCode = { const token: AuthorizationCode = {
code: uuidv4(), code: uuidv4(),
client, client,
@@ -65,7 +85,35 @@ export class UsersService {
}; };
const t = this.tokenRepo.create(token); const t = this.tokenRepo.create(token);
await this.tokenRepo.save(t); await this.tokenRepo.save(t);
return { code: token.code }; return token;
}
async loginWithSessionKey(sessionKey: string, clientId: string) {
const client = await this.clientService.getClientById(clientId);
if (!client) {
this.logger.error(`Client ${clientId} not found`);
throw new HttpException('Invalid client', 401);
}
const session = await this.sessionRepo.findOneByOrFail({ id: sessionKey });
if (!session) {
throw new HttpException('Invalid session key', 401);
}
const user = await this.userRepo.findById(session.user.id);
if (!user) {
throw new HttpException('Invalid session key', 401);
}
if (!user.isActive) {
throw new HttpException('User is not active', 401);
}
const token = this.createAuthToken(user, client);
return token;
} }
async verifyToken({ clientId, clientSecret, code, grantType }: any) { async verifyToken({ clientId, clientSecret, code, grantType }: any) {

View File

@@ -1,7 +1,9 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './auth/login/login.component';
import { RegisterComponent } from './auth/register/register.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: '', component: LoginComponent }, { path: '', component: LoginComponent },
]; ];

View File

@@ -144,6 +144,12 @@ body {
box-shadow: 0px 2px 2px #5C5696; box-shadow: 0px 2px 2px #5C5696;
cursor: pointer; cursor: pointer;
transition: .2s; transition: .2s;
&:disabled {
background: #e3e3e3;
color: grey;
pointer-events: none;
}
} }
.login__submit:active, .login__submit:active,
@@ -184,4 +190,16 @@ body {
.social-login__icon:hover { .social-login__icon:hover {
transform: scale(1.5); transform: scale(1.5);
} }
.login-register {
margin-top: 24px;
color: white;
text-align: end;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}

View File

@@ -11,10 +11,11 @@
<i class="login__icon fas fa-lock safe"></i> <i class="login__icon fas fa-lock safe"></i>
<input type="password" formControlName="password" class="login__input" placeholder="Password"> <input type="password" formControlName="password" class="login__input" placeholder="Password">
</div> </div>
<button class="button login__submit" (click)="login()" [disabled]="!client_id"> <button class="button login__submit" (click)="login()" [disabled]="!client_id || loginForm.invalid">
<span class="button__text">Log In Now</span> <span class="button__text">Log In Now</span>
<i class="button__icon fas fa-chevron-right"></i> <i class="button__icon fas fa-chevron-right"></i>
</button> </button>
<div class="login-register" (click)="toRegister()" >Register...</div>
</form> </form>
</div> </div>
<div class="screen__background"> <div class="screen__background">

View File

@@ -1,9 +1,9 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { environment } from '../../environments/environment'; import { environment } from '../../../environments/environment';
import { HotToastService } from '@ngxpert/hot-toast'; import { HotToastService } from '@ngxpert/hot-toast';
@Component({ @Component({
@@ -11,12 +11,13 @@ import { HotToastService } from '@ngxpert/hot-toast';
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule], imports: [CommonModule, FormsModule, ReactiveFormsModule],
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrl: './login.component.scss' styleUrl: '../auth.scss'
}) })
export class LoginComponent { export class LoginComponent {
private http: HttpClient = inject(HttpClient); private http: HttpClient = inject(HttpClient);
private route: ActivatedRoute = inject(ActivatedRoute); private route: ActivatedRoute = inject(ActivatedRoute);
private toast: HotToastService = inject(HotToastService); private toast: HotToastService = inject(HotToastService);
private router: Router = inject(Router);
redirectUri = null; redirectUri = null;
client: string = ""; client: string = "";
@@ -29,6 +30,27 @@ export class LoginComponent {
constructor() { constructor() {
this.getclient(); this.getclient();
this.loginWithSessionId();
}
loginWithSessionId() {
const id = window.localStorage.getItem("auth_sesion_key");
if (!id ||id.length < 2) { return; }
this.toast.loading('Logging in...');
this.http.post(environment.api_url + 'auth/login-with-session-id', {
code: id,
client_id: this.client_id
}).subscribe({
next: (data) => {
if (data["code"] != null) {
location.href = this.redirectUri + "?code=" + data["code"];
}
},
error: (error) => {
console.error(error);
}
});
} }
@@ -36,15 +58,11 @@ export class LoginComponent {
const params = (this.route.snapshot.queryParamMap as any)["params"]; const params = (this.route.snapshot.queryParamMap as any)["params"];
this.redirectUri = params.redirect_uri; this.redirectUri = params.redirect_uri;
this.client_id = params.client_id; this.client_id = params.client_id;
this.route.snapshot.queryParamMap.keys.forEach((key) => {
console.log(key, this.route.snapshot.queryParamMap.get(key));
});
this.http.get<any>(environment.api_url + 'auth/', { this.http.get<any>(environment.api_url + 'auth/', {
params params
}).subscribe({ }).subscribe({
next: (client) => { next: (client) => {
console.log(client)
this.client = client.clientName; this.client = client.clientName;
}, },
error: (error) => { error: (error) => {
@@ -55,9 +73,11 @@ export class LoginComponent {
} }
login() { login() {
this.toast.loading('Logging in...');
this.http.post(environment.api_url + 'auth/login?'+ 'client_id=' + this.client_id, this.loginForm.value).subscribe({ this.http.post(environment.api_url + 'auth/login?'+ 'client_id=' + this.client_id, this.loginForm.value).subscribe({
next: (data) => { next: (data) => {
if (data["code"] != null) { if (data["code"] != null) {
window.localStorage.setItem("auth_sesion_key", data["session_key"]);
location.href = this.redirectUri + "?code=" + data["code"]; location.href = this.redirectUri + "?code=" + data["code"];
} }
}, },
@@ -67,4 +87,8 @@ export class LoginComponent {
} }
}) })
} }
toRegister() {
this.router.navigate(['/register'], { queryParams: this.route.snapshot.queryParams });
}
} }

View File

@@ -0,0 +1,40 @@
<div class="container">
<div class="screen">
<div class="screen__content">
<h1>{{ client }}</h1>
<form class="login" [formGroup]="registerForm" style="padding-top: 24px;" >
<div class="login__field">
<!-- <i class="login__icon fas fa-lock safe"></i> -->
<input type="text" formControlName="firstName" class="login__input" autocomplete="given-name" placeholder="Firstname">
</div>
<div class="login__field">
<!-- <i class="login__icon fas fa-lock safe"></i> -->
<input type="text" formControlName="lastName" class="login__input" autocomplete="family-name" placeholder="Lastname">
</div>
<div class="login__field">
<i class="login__icon fas fa-user user"></i>
<input formControlName="username" type="text" class="login__input" autocomplete="email" placeholder="User name / Email">
</div>
<div class="login__field">
<i class="login__icon fas fa-lock safe"></i>
<input type="password" formControlName="password" autocomplete="new-password" class="login__input" placeholder="Password">
</div>
<div class="login__field">
<i class="login__icon fas fa-lock safe"></i>
<input type="password" formControlName="repeatPassword" autocomplete="new-password" class="login__input" placeholder="Repeat password">
</div>
<button class="button login__submit" (click)="register()" [disabled]="!client_id || registerForm.invalid">
<span class="button__text">Register</span>
<i class="button__icon fas fa-chevron-right"></i>
</button>
<div class="login-register" (click)="toLogin()" >Login...</div>
</form>
</div>
<div class="screen__background">
<span class="screen__background__shape screen__background__shape4"></span>
<span class="screen__background__shape screen__background__shape3"></span>
<span class="screen__background__shape screen__background__shape2"></span>
<span class="screen__background__shape screen__background__shape1"></span>
</div>
</div>
</div>

View File

@@ -0,0 +1,80 @@
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { HotToastService } from '@ngxpert/hot-toast';
import { environment } from '../../../environments/environment';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-register',
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule],
templateUrl: './register.component.html',
styleUrl: '../auth.scss'
})
export class RegisterComponent {
private http: HttpClient = inject(HttpClient);
private route: ActivatedRoute = inject(ActivatedRoute);
private toast: HotToastService = inject(HotToastService);
private router: Router = inject(Router);
redirectUri = null;
client: string = "";
client_id = null;
registerForm = new FormGroup({
username: new FormControl('', [Validators.required, Validators.email, Validators.maxLength(100)]),
password: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]),
repeatPassword: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]),
firstName: new FormControl('', [Validators.required, Validators.maxLength(100)]),
lastName: new FormControl('', [Validators.required, Validators.maxLength(100)]),
})
constructor() {
this.getclient();
}
getclient() {
const params = (this.route.snapshot.queryParamMap as any)["params"];
this.redirectUri = params.redirect_uri;
this.client_id = params.client_id;
this.http.get<any>(environment.api_url + 'auth/', {
params
}).subscribe({
next: (client) => {
this.client = client.clientName;
},
error: (error) => {
console.error(error);
this.toast.error('Invalid client');
}
})
}
register() {
if (this.registerForm.value.password != this.registerForm.value.repeatPassword) {
this.toast.error('Passwords do not match');
return;
}
this.http.post(environment.api_url + 'auth/register?'+ 'client_id=' + this.client_id, this.registerForm.value).pipe(
this.toast.observe({
loading: 'Registering...',
success: 'Registration successfull'
})
).subscribe({
next: () => {
this.router.navigate(['/login'], { queryParams: this.route.snapshot.queryParams });
},
error: (error) => {
console.error(error);
this.toast.error('Registration not successfull');
}
})
}
toLogin() {
this.router.navigate(['/login'], { queryParams: this.route.snapshot.queryParams });
}
}

View File

@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});