-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.service.ts
164 lines (157 loc) · 5.36 KB
/
auth.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import {
ForbiddenException,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { forumConfig } from 'src/config/forum.config';
import { authExceptions } from '../config/auth.exceptions';
import { LoginResource } from '../resources/login.resource';
import { SessionResource } from '../resources/session.resource';
import { JwtService } from '@nestjs/jwt';
import { JwtResource } from '../resources/jwt.resource';
import { HttpService } from 'src/http/http.service';
import { XmlJsService } from 'src/xml-api/xml-js.service';
import { EncodingService } from 'src/encoding/encoding.service';
import { UsersService } from 'src/users/services/users.service';
@Injectable()
export class AuthService {
constructor(
private readonly httpService: HttpService,
private readonly xmljs: XmlJsService,
private readonly jwtService: JwtService,
private readonly encodingService: EncodingService,
private readonly usersService: UsersService,
) {}
async login(loginResource: LoginResource): Promise<JwtResource> {
Logger.log(
`User '${loginResource.username}' is attempting to log in.`,
this.constructor.name,
);
const lifetime =
typeof loginResource.lifetime === 'number'
? loginResource.lifetime
: parseInt(loginResource.lifetime);
try {
const username = this.encodingService.encodeLoginCredentials(
loginResource.username,
);
const password = this.encodingService.encodeLoginCredentials(
loginResource.password,
);
const payload = `login_username=${username}&login_password=${password}&login_lifetime=${lifetime}`;
const { data } = await this.httpService.post(
forumConfig.LOGIN_URL,
payload,
);
this.checkForLoginSuccess(data);
const cookieUrl = this.getSessionCookieUrl(data);
const cookie = await this.getSessionCookie(cookieUrl);
const session = await this.createSession(cookie);
Logger.log(
`User '${session.username}' has signed in.`,
this.constructor.name,
);
return {
access_token: this.jwtService.sign(session, {
expiresIn: lifetime,
}),
} as JwtResource;
} catch (error) {
Logger.log(`Login attempt failed (${error}).`, this.constructor.name);
throw error;
}
}
/**
* Checks the login response for signs of success. Throws an exception if
* login was not successful.
*/
checkForLoginSuccess(data: string) {
if (/Erfolgreich eingeloggt/.test(data)) {
return true;
} else if (/Fehler\sbeim\sEinloggen/.test(data)) {
throw authExceptions.login.wrongCredentials;
} else {
throw authExceptions.login.unknown;
}
}
/**
* Searches the text response for the session cookie location.
* @param data The text data to search.
* @returns Returns the URL of the post-login page from where the
* session cookie need to be retrieved.
*/
getSessionCookieUrl(data: string): string {
const iframeUriMatches = data.match(/(?:(<iframe\ssrc=')(.*?)?('))/);
if (iframeUriMatches && iframeUriMatches.length >= 3) {
return `https:${iframeUriMatches[2]}`;
} else {
throw new Error('Unable to retrieve session cookie location.');
}
}
/**
* Gets the two session cookie from the given URL.
* @param url The url that should be called to retrieve the cookie.
* @returns The object containing the session cookie.
*/
async getSessionCookie(url: string) {
const { headers } = await this.httpService.get(url);
const cookie = headers['set-cookie'];
if (cookie && cookie.length >= 2) {
// Since the first cookie tends to not work, we use the second one
return `${cookie[1].split(';')[0]}`;
} else {
throw new Error('Did not receive two session cookies.');
}
}
/**
* Calls the forum landing page with the given session cookie and creates a new session
* using the retrieved details.
* @param cookie The session cookie.
* @returns The session.
*/
async createSession(cookie: string): Promise<SessionResource> {
try {
const userId = await this.getUserId(cookie);
const { name, avatarUrl, locked, status } =
await this.usersService.findById(userId);
// If the use account has been locked permanently, the login should fail.
// We check both locked and status since locked will also be true if the account
// has been locked temporarily.
if (locked && status === 'gesperrt') {
throw authExceptions.login.lockedPermanently;
}
const session: Partial<SessionResource> = {
userId,
username: name,
avatarUrl,
cookie,
};
return session as SessionResource;
} catch (error) {
if (error instanceof ForbiddenException) throw error;
throw new UnauthorizedException(error.message);
}
}
/**
* Calls the 'boards.php' endpoint to extract the user id.
* @param cookie The session cookie.
*/
async getUserId(cookie: string): Promise<string> {
const { data } = await this.httpService.get(
`${forumConfig.API_URL}boards.php`,
{
cookie,
},
);
const xmlDocument = this.xmljs.parseXml(data);
const userId = this.xmljs.getAttribute(
'current-user-id',
xmlDocument.elements[0],
);
if (!userId) {
throw new Error('Unable to retrieve user id.');
}
return userId;
}
}