Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ The following table shows the various configurations options which can be set an
| UPDATE_INTERVAL | Number | 30 | The interval in minutes to update the attendance of students who are late arrivals |
| DRY_RUN | Boolean | false | Determines whether dry run mode is enabled. In dry run mode, no changes are made and instead logged to the console |
| RUN_IMMEDIATELY | Boolean | true if running under development mode, otherwise false | Determines whether attendance should get updated right when the program runs or if it should wait for the next occurance in the schedule |
| LOG_LEVEL | String | debug if running under development mode, otherwise info | Determines the minimum severity of the output logs |

## Usage

Expand Down Expand Up @@ -125,6 +126,8 @@ Within the application, all time related strings are parsed using [Moment.js](ht
- H:m Z
- h a Z
- H Z
- ha Z
- h:ma Z

### Symbol Meanings

Expand Down
5 changes: 2 additions & 3 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ services:
build:
context: ./
target: development
ports:
- "3000:3000"
environment:
SCHOOLPASS_USERNAME: username
SCHOOLPASS_PASSWORD: password
Expand All @@ -16,4 +14,5 @@ services:
ATTENDANCE_END: 8am
SCHOOL_DISMISSAL_TIME: 3pm
UPDATE_INTERVAL: 30
DRY_RUN: true
LOG_LEVEL: info
DRY_RUN: true
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ services:
build:
context: ./
target: production
ports:
- "3000:3000"
environment:
SCHOOLPASS_USERNAME:
SCHOOLPASS_PASSWORD:
UNIFI_ACCESS_SERVER: https://server:12445
UNIFI_ACCESS_API_TOKEN:
volumes:
- ./logs:/usr/src/app/logs
22 changes: 21 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "unifi-access-attendance",
"version": "0.0.2",
"version": "0.0.3",
"description": "An automated way to handle daily attendance using badge scans from Unifi Access with SchoolPass",
"scripts": {
"start": "node dist/server.js",
Expand All @@ -10,7 +10,7 @@
"lint": "eslint \"**/*.{js,ts}\"",
"lint:fix": "eslint --fix \"**/*.{js,ts}\"",
"docker:build": "docker build -t unifi-access-attendance .",
"docker:run": "docker run -d -p 3000:3000 --name unifi-access-attendance unifi-access-attendance",
"docker:run": "docker run -d --name unifi-access-attendance unifi-access-attendance",
"postdocker:build": "docker image prune -f --filter label=stage=intermediate"
},
"author": "John Arrandale",
Expand Down Expand Up @@ -41,6 +41,7 @@
"dotenv": "^16.4.5",
"fast-safe-stringify": "^2.1.1",
"moment": "^2.30.1",
"node-cache": "^5.1.2",
"node-schedule": "^2.1.1",
"winston": "^3.13.0"
},
Expand All @@ -64,4 +65,4 @@
"lint-staged": {
"*.{js,ts}": "eslint --cache --fix"
}
}
}
7 changes: 6 additions & 1 deletion src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import "dotenv/config";
import moment from "moment";

const timeFormat = ["h:m a Z", "H:m Z", "h a Z", "H Z"];
const timeFormat = ["h:m a Z", "H:m Z", "h a Z", "H Z", "ha Z", "h:ma Z"];

export default {
production: process.env.NODE_ENV === "production",
logLevel: (() => {
if (process.env.LOG_LEVEL) return process.env.LOG_LEVEL;

return process.env.NODE_ENV === "production" ? "info" : "debug";
})(),
schoolPass: {
username: process.env.SCHOOLPASS_USERNAME || "",
password: process.env.SCHOOLPASS_PASSWORD || "",
Expand Down
81 changes: 59 additions & 22 deletions src/lib/Logger.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import { createLogger, format, transports } from "winston";
import {
createLogger,
format,
transports,
Logger,
LeveledLogMethod
} from "winston";
import { setGlobalConfig } from "axios-logger";
import stringify from "fast-safe-stringify";
import axios from "axios";
import * as AxiosLogger from "axios-logger";

import environment from "../environment";

const logger = createLogger({
level: "info",
level: environment.logLevel,
format: format.json(),
defaultMeta: {
service: "unifi-access-attendance"
},
transports: []
transports: [
new transports.Console({
format: format.combine(
format.timestamp(),
format.simple(),
format.colorize(),
format.printf(options => {
const args = options[Symbol.for("splat")]?.filter(
(arg: any) => !(arg instanceof Error)
);

const argsString = args?.map(stringify).join(" ");

return `${options.timestamp} ${options.level} [${options.service}]${options.label ? " [" + options.label + "]" : ""} ${options.message}${argsString ? " " + argsString : ""}${options.stack ? "\n" + options.stack : ""}`;
})
)
}),
new transports.File({
filename: "./logs/unifi-attendance.log",
format: format.combine(format.timestamp(), format.json())
})
]
});

setGlobalConfig({
Expand All @@ -17,24 +48,30 @@ setGlobalConfig({
data: true
});

logger.add(
new transports.Console({
level: "debug",
format: format.combine(
format.timestamp(),
format.simple(),
format.colorize(),
format.printf(options => {
const args = options[Symbol.for("splat")]?.filter(
(arg: any) => !(arg instanceof Error)
);

const argsString = args?.map(stringify).join(" ");

return `${options.timestamp} ${options.level} [${options.service}]${options.label ? " [" + options.label + "]" : ""} ${options.message}${argsString ? " " + argsString : ""}${options.stack ? "\n" + options.stack : ""}`;
})
)
})
);
export const addAxiosLoggerInterceptors = (
http: axios.AxiosInstance,
logger: Logger
) => {
const wrapAxiosLogger = (
axiosLogger: any,
logger: Logger,
level: LeveledLogMethod
) => {
return (msg: any) =>
axiosLogger(msg, {
logger: level.bind(logger)
});
};

http.interceptors.request.use(
wrapAxiosLogger(AxiosLogger.requestLogger, logger, logger.debug),
wrapAxiosLogger(AxiosLogger.errorLogger, logger, logger.error)
);

http.interceptors.response.use(
wrapAxiosLogger(AxiosLogger.responseLogger, logger, logger.debug),
wrapAxiosLogger(AxiosLogger.errorLogger, logger, logger.error)
);
};

export default logger;
73 changes: 54 additions & 19 deletions src/lib/SchoolPassAPI.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import axios, { AxiosInstance } from "axios";
import winston from "winston";
import * as AxiosLogger from "axios-logger";

import logger from "./Logger";
import logger, { addAxiosLoggerInterceptors } from "./Logger";

const configURL = "https://schoolpass.cloud/assets/runtime.config.json";

Expand Down Expand Up @@ -85,6 +84,40 @@ export interface SchoolPassStudentAttendance {
wellnessStatus: number;
}

/**
* Outlines a student's profile
*/
export interface SchoolPassStudentProfile {
notificationSettings: [
{
notificationMode: number;
userNotificationType: number;
allow: boolean;
}
];
dismissalLocationId: number;
gradeId: number;
sitePrefix: string;
siteName: string;
quickPIN: string;
tags: unknown;
user: {
userType: number;
internalId: number;
};
firstName: string;
lastName: string;
dateOfBirth: unknown;
phoneNumber: unknown;
address: unknown;
email: string;
externalId: string;
created: unknown;
userDefinedField1: unknown;
optOutEmail: boolean;
quickPINForUser: unknown;
}

/**
* Specifies the classroom attendance types
*/
Expand Down Expand Up @@ -189,26 +222,11 @@ export class SchoolPassAPI {
}
);

http.interceptors.request.use(
AxiosLogger.requestLogger,
AxiosLogger.errorLogger
);
http.interceptors.response.use(
AxiosLogger.responseLogger,
AxiosLogger.errorLogger
);

this.http = http;
this.homebaseHttp = axios.create();

this.homebaseHttp.interceptors.request.use(
AxiosLogger.requestLogger,
AxiosLogger.errorLogger
);
this.homebaseHttp.interceptors.response.use(
AxiosLogger.responseLogger,
AxiosLogger.errorLogger
);
addAxiosLoggerInterceptors(http, this.logger);
addAxiosLoggerInterceptors(this.homebaseHttp, this.logger);
}

/**
Expand Down Expand Up @@ -341,6 +359,23 @@ export class SchoolPassAPI {
return res.data;
}

/**
* Retrieves a student's user profile
* @param studentId The id of the student
* @returns A {@link SchoolPassStudentProfile} object
*/
async getStudentProfile(
studentId: number
): Promise<SchoolPassStudentProfile> {
const res = await this.http.get("Student/profile", {
params: {
studentId
}
});

return res.data;
}

/**
* Retrieves the classroom attendance data for a specified classroom
* @param locationId The location id of the classroom
Expand Down
12 changes: 2 additions & 10 deletions src/lib/UnifiAccessAPI.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import axios, { AxiosInstance } from "axios";
import winston from "winston";
import * as AxiosLogger from "axios-logger";
import https from "https";

import logger from "./Logger";
import logger, { addAxiosLoggerInterceptors } from "./Logger";
import environment from "../environment";

/**
Expand Down Expand Up @@ -155,14 +154,7 @@ export class UnifiAccessAPI {
})
});

http.interceptors.request.use(
AxiosLogger.requestLogger,
AxiosLogger.errorLogger
);
http.interceptors.response.use(
AxiosLogger.responseLogger,
AxiosLogger.errorLogger
);
addAxiosLoggerInterceptors(http, this.logger);

http.defaults.baseURL = new URL(
"/api/v1/developer/",
Expand Down
Loading