finki-auth 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FINKI Auth
2
2
 
3
- NPM (Node) package for managing authentication and sessions for FCSE CAS and the other services. Uses an HTTP client under the hood, and not a full browser (such as Puppeteer).
3
+ Node.js package for managing authentication and cookies for FCSE's services, built using [axios](https://github.com/axios/axios).
4
4
 
5
5
  ## Features
6
6
 
@@ -21,16 +21,43 @@ You can add the package to your NPM project by running `npm i finki-auth`.
21
21
  ## Example
22
22
 
23
23
  ```ts
24
- import { CasAuthentication, Service } from "finki-auth";
24
+ import {
25
+ CasAuthentication,
26
+ Service,
27
+ isCookieValid,
28
+ isCookieHeaderValid,
29
+ } from "finki-auth";
25
30
 
26
- const auth = new CasAuthentication(credentials.username, credentials.password);
27
- const rawCookies = await auth.authenticate(Service.COURSES);
31
+ const credentials = {
32
+ username: "example",
33
+ password: "secret_password",
34
+ };
28
35
 
29
- const cookies: Record<string, string> = {};
36
+ const auth = new CasAuthentication(credentials);
30
37
 
31
- for (const { key, value } of rawCookies) {
32
- cookies[key] = value;
33
- }
38
+ await auth.authenticate(Service.COURSES);
39
+
40
+ // Get array of cookie objects
41
+ const cookies = await auth.getCookie(Service.COURSES);
42
+
43
+ // Get cookie header directly for sending requests
44
+ const cookieHeader = await auth.buildCookieHeader(Service.COURSES);
45
+
46
+ // Check if the cookie is still valid, and if not, call `authenticate` again
47
+ const isCookieValid = await auth.isCookieValid(Service.COURSES);
48
+
49
+ if (!isCookieValid) await auth.authenticate();
50
+
51
+ // There are also some utility functions available:
52
+ const isCookieValidStandalone = await isCookieValid({
53
+ service: Service.COURSES,
54
+ cookies,
55
+ });
56
+
57
+ const isCookieHeaderValidStandalone = await isCookieHeaderValid({
58
+ service: Service.COURSES,
59
+ cookieHeader,
60
+ });
34
61
  ```
35
62
 
36
63
  ## License
@@ -0,0 +1,16 @@
1
+ import { Service } from './lib/Service.js';
2
+ export declare class CasAuthentication {
3
+ private readonly password;
4
+ private readonly session;
5
+ private readonly username;
6
+ constructor({ password, username }: {
7
+ password: string;
8
+ username: string;
9
+ });
10
+ private static readonly getFullLoginUrl;
11
+ readonly authenticate: (service: Service) => Promise<void>;
12
+ readonly buildCookieHeader: (service: Service) => Promise<string>;
13
+ readonly getCookie: (service: Service) => Promise<import("tough-cookie").Cookie[]>;
14
+ readonly isCookieValid: (service: Service) => Promise<boolean>;
15
+ private readonly getFormData;
16
+ }
@@ -3,13 +3,14 @@ import { wrapper } from 'axios-cookiejar-support';
3
3
  import { JSDOM } from 'jsdom';
4
4
  import { CookieJar } from 'tough-cookie';
5
5
  import { z } from 'zod';
6
- import { SERVICE_LOGIN_URLS } from './constants.js';
6
+ import { SERVICE_LOGIN_URLS, SERVICE_URLS } from './constants.js';
7
7
  import { Service } from './lib/Service.js';
8
+ import { getCookieValidity } from './utils.js';
8
9
  export class CasAuthentication {
9
10
  password;
10
11
  session;
11
12
  username;
12
- constructor(username, password) {
13
+ constructor({ password, username }) {
13
14
  this.username = username;
14
15
  this.password = password;
15
16
  const cookieJar = new CookieJar();
@@ -19,8 +20,14 @@ export class CasAuthentication {
19
20
  });
20
21
  this.session = wrapper(client);
21
22
  }
23
+ static getFullLoginUrl = (service) => {
24
+ if (service === Service.CAS) {
25
+ return SERVICE_LOGIN_URLS[Service.CAS];
26
+ }
27
+ return `${SERVICE_LOGIN_URLS[Service.CAS]}?service=${encodeURIComponent(SERVICE_LOGIN_URLS[service])}`;
28
+ };
22
29
  authenticate = async (service) => {
23
- const fullUrl = `${SERVICE_LOGIN_URLS[Service.CAS]}?service=${encodeURIComponent(SERVICE_LOGIN_URLS[service])}`;
30
+ const fullUrl = CasAuthentication.getFullLoginUrl(service);
24
31
  const initialRequest = await this.session.get(fullUrl);
25
32
  const { data } = z.string().safeParse(initialRequest.data);
26
33
  if (!data) {
@@ -30,14 +37,24 @@ export class CasAuthentication {
30
37
  const hiddenInputs = window.document.querySelectorAll('input[type="hidden"]');
31
38
  const urlSearchParams = this.getFormData(hiddenInputs);
32
39
  await this.session.post(fullUrl, urlSearchParams);
33
- return this.getCookies(service);
34
40
  };
35
- getCookies = async (service) => {
36
- const casCookies = (await this.session.defaults.jar?.getCookies(SERVICE_LOGIN_URLS[Service.CAS])) ?? [];
37
- const serviceCookies = service
38
- ? ((await this.session.defaults.jar?.getCookies(SERVICE_LOGIN_URLS[service])) ?? [])
39
- : [];
40
- return [...casCookies, ...serviceCookies];
41
+ buildCookieHeader = async (service) => {
42
+ const cookies = await this.getCookie(service);
43
+ return cookies.map(({ key, value }) => `${key}=${value}`).join('; ');
44
+ };
45
+ getCookie = async (service) => {
46
+ const serviceLoginUrl = SERVICE_LOGIN_URLS[service];
47
+ const serviceCookies = await this.session.defaults.jar?.getCookies(serviceLoginUrl);
48
+ return serviceCookies ?? [];
49
+ };
50
+ isCookieValid = async (service) => {
51
+ const url = SERVICE_URLS[service];
52
+ const cookies = await this.getCookie(service);
53
+ const jar = new CookieJar();
54
+ for (const cookie of cookies) {
55
+ await jar.setCookie(cookie, url);
56
+ }
57
+ return await getCookieValidity({ cookieJar: jar, service });
41
58
  };
42
59
  getFormData = (inputs) => {
43
60
  const urlSearchParams = new URLSearchParams();
@@ -1,5 +1,5 @@
1
1
  export declare const SERVICE_URLS: {
2
- readonly cas: "https://cas.finki.ukim.mk";
2
+ readonly cas: "https://cas.finki.ukim.mk/cas";
3
3
  readonly consultations: "https://consultations.finki.ukim.mk";
4
4
  readonly courses: "https://courses.finki.ukim.mk";
5
5
  readonly diplomas: "http://diplomski.finki.ukim.mk";
@@ -16,8 +16,8 @@ export declare const SERVICE_LOGIN_URLS: {
16
16
  readonly masters: "https://magisterski.finki.ukim.mk/login";
17
17
  readonly old_courses: "https://oldcourses.finki.ukim.mk/login/index.php";
18
18
  };
19
- export declare const SERVICE_USER_ELEMENT_SELECTORS: {
20
- readonly cas: "";
19
+ export declare const SERVICE_SUCCESS_SELECTORS: {
20
+ readonly cas: "div.success";
21
21
  readonly consultations: "a#username";
22
22
  readonly courses: "span.usertext.me-1";
23
23
  readonly diplomas: "#logoutForm > ul > li:nth-child(1) > a";
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Service } from './lib/Service.js';
2
2
  export const SERVICE_URLS = {
3
- [Service.CAS]: 'https://cas.finki.ukim.mk',
3
+ [Service.CAS]: 'https://cas.finki.ukim.mk/cas',
4
4
  [Service.CONSULTATIONS]: 'https://consultations.finki.ukim.mk',
5
5
  [Service.COURSES]: 'https://courses.finki.ukim.mk',
6
6
  [Service.DIPLOMAS]: 'http://diplomski.finki.ukim.mk',
@@ -17,8 +17,8 @@ export const SERVICE_LOGIN_URLS = {
17
17
  [Service.MASTERS]: 'https://magisterski.finki.ukim.mk/login',
18
18
  [Service.OLD_COURSES]: 'https://oldcourses.finki.ukim.mk/login/index.php',
19
19
  };
20
- export const SERVICE_USER_ELEMENT_SELECTORS = {
21
- [Service.CAS]: '',
20
+ export const SERVICE_SUCCESS_SELECTORS = {
21
+ [Service.CAS]: 'div.success',
22
22
  [Service.CONSULTATIONS]: 'a#username',
23
23
  [Service.COURSES]: 'span.usertext.me-1',
24
24
  [Service.DIPLOMAS]: '#logoutForm > ul > li:nth-child(1) > a',
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { CasAuthentication } from './auth.js';
1
+ export { CasAuthentication } from './authentication.js';
2
2
  export { Service } from './lib/Service.js';
3
- export { isCookieValid } from './session.js';
3
+ export { isCookieHeaderValid, isCookieValid } from './utils.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { CasAuthentication } from './auth.js';
1
+ export { CasAuthentication } from './authentication.js';
2
2
  export { Service } from './lib/Service.js';
3
- export { isCookieValid } from './session.js';
3
+ export { isCookieHeaderValid, isCookieValid } from './utils.js';
@@ -0,0 +1,14 @@
1
+ import { type Cookie, CookieJar } from 'tough-cookie';
2
+ import type { Service } from './lib/Service.js';
3
+ export declare const getCookieValidity: ({ cookieJar, service, }: {
4
+ cookieJar: CookieJar;
5
+ service: Service;
6
+ }) => Promise<boolean>;
7
+ export declare const isCookieValid: ({ cookies, service, }: {
8
+ cookies: Cookie[];
9
+ service: Service;
10
+ }) => Promise<boolean>;
11
+ export declare const isCookieHeaderValid: ({ cookieHeader, service, }: {
12
+ cookieHeader: string;
13
+ service: Service;
14
+ }) => Promise<boolean>;
package/dist/utils.js ADDED
@@ -0,0 +1,45 @@
1
+ import axios from 'axios';
2
+ import { wrapper } from 'axios-cookiejar-support';
3
+ import { JSDOM } from 'jsdom';
4
+ import { CookieJar } from 'tough-cookie';
5
+ import z from 'zod';
6
+ import { SERVICE_SUCCESS_SELECTORS, SERVICE_URLS } from './constants.js';
7
+ export const getCookieValidity = async ({ cookieJar, service, }) => {
8
+ const url = SERVICE_URLS[service];
9
+ const userElementSelector = SERVICE_SUCCESS_SELECTORS[service];
10
+ const client = wrapper(axios.create({ jar: cookieJar }));
11
+ const response = await client.get(url);
12
+ const html = z.string().parse(response.data);
13
+ const { window } = new JSDOM(html);
14
+ const userElement = window.document.querySelector(userElementSelector);
15
+ switch (userElement?.textContent) {
16
+ case undefined:
17
+ case 'Најава':
18
+ return false;
19
+ default:
20
+ return true;
21
+ }
22
+ };
23
+ export const isCookieValid = async ({ cookies, service, }) => {
24
+ const url = SERVICE_URLS[service];
25
+ const jar = new CookieJar();
26
+ for (const cookie of cookies) {
27
+ await jar.setCookie(cookie, url);
28
+ }
29
+ return getCookieValidity({ cookieJar: jar, service });
30
+ };
31
+ export const isCookieHeaderValid = async ({ cookieHeader, service, }) => {
32
+ const url = SERVICE_URLS[service];
33
+ const jar = new CookieJar();
34
+ const cookies = cookieHeader
35
+ ? cookieHeader.split('; ').map((cookie) => {
36
+ const [key, ...valParts] = cookie.split('=');
37
+ const value = valParts.join('=');
38
+ return { key, value };
39
+ })
40
+ : [];
41
+ for (const { key, value } of cookies) {
42
+ await jar.setCookie(`${key}=${value}`, url);
43
+ }
44
+ return getCookieValidity({ cookieJar: jar, service });
45
+ };
package/package.json CHANGED
@@ -63,7 +63,7 @@
63
63
  "commit": "cz",
64
64
  "commitlint": "commitlint --edit",
65
65
  "format": "eslint --ignore-pattern \"**/test/*\" --fix .",
66
- "lint": "eslint . --ignore-pattern \"**/test/*\" --no-warn-ignored --cache",
66
+ "lint": "tsc --noEmit && eslint . --ignore-pattern \"**/test/*\" --no-warn-ignored --cache",
67
67
  "prepare": "husky",
68
68
  "package": "npm run build && npm pack",
69
69
  "release": "npm run build && semantic-release",
@@ -72,5 +72,5 @@
72
72
  },
73
73
  "type": "module",
74
74
  "types": "dist/index.d.ts",
75
- "version": "1.6.0"
75
+ "version": "1.8.0"
76
76
  }
package/dist/auth.d.ts DELETED
@@ -1,10 +0,0 @@
1
- import { Service } from './lib/Service.js';
2
- export declare class CasAuthentication {
3
- private readonly password;
4
- private readonly session;
5
- private readonly username;
6
- constructor(username: string, password: string);
7
- authenticate: (service: Service) => Promise<import("tough-cookie").Cookie[]>;
8
- private readonly getCookies;
9
- private readonly getFormData;
10
- }
@@ -1,7 +0,0 @@
1
- import type { CookieJar } from 'tough-cookie';
2
- import 'axios';
3
- declare module 'axios' {
4
- interface AxiosRequestConfig {
5
- jar?: CookieJar;
6
- }
7
- }
package/dist/lib/axios.js DELETED
@@ -1 +0,0 @@
1
- import 'axios';
package/dist/session.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { Service } from './lib/Service.js';
2
- export declare const isCookieValid: (service: Service, cookies: string) => Promise<boolean>;
package/dist/session.js DELETED
@@ -1,29 +0,0 @@
1
- import axios from 'axios';
2
- import { wrapper } from 'axios-cookiejar-support';
3
- import { JSDOM } from 'jsdom';
4
- import { CookieJar } from 'tough-cookie';
5
- import { z } from 'zod';
6
- import { SERVICE_URLS, SERVICE_USER_ELEMENT_SELECTORS } from './constants.js';
7
- export const isCookieValid = async (service, cookies) => {
8
- const url = SERVICE_URLS[service];
9
- const userElementSelector = SERVICE_USER_ELEMENT_SELECTORS[service];
10
- const jar = new CookieJar();
11
- if (cookies) {
12
- const cookiePairs = cookies.split('; ');
13
- for (const pair of cookiePairs) {
14
- await jar.setCookie(pair, url);
15
- }
16
- }
17
- const client = wrapper(axios.create({ jar }));
18
- const response = await client.get(url);
19
- const html = z.string().parse(response.data);
20
- const { window } = new JSDOM(html);
21
- const userElement = window.document.querySelector(userElementSelector);
22
- switch (userElement?.textContent) {
23
- case undefined:
24
- case 'Најава':
25
- return false;
26
- default:
27
- return true;
28
- }
29
- };