├── .dockerignore ├── .docs ├── images │ ├── desktop_0.png │ ├── desktop_1.png │ ├── desktop_2.png │ ├── desktop_3.png │ ├── email_example.png │ ├── escpos_example.jpg │ ├── kiosk_example.png │ ├── mobile_0.png │ ├── mobile_1.png │ ├── mobile_2.png │ ├── mobile_3.png │ └── pdf_example.png └── oidc │ ├── authentik │ ├── README.md │ └── images │ │ ├── create_application.png │ │ ├── create_provider_1.png │ │ ├── create_provider_2.png │ │ └── create_provider_3.png │ ├── keycloak │ ├── README.md │ └── images │ │ ├── client_secret.png │ │ ├── client_settings_1.png │ │ ├── client_settings_2.png │ │ ├── create_client.png │ │ └── create_realm.png │ ├── uid │ ├── README.md │ └── images │ │ ├── uid_add_oidc_app.png │ │ ├── uid_app_launcher.png │ │ ├── uid_custom_app.png │ │ ├── uid_demo_sign_in.png │ │ ├── uid_manager_portal.png │ │ ├── uid_oidc.png │ │ ├── uid_oidc_sign_in_success.png │ │ ├── uid_sign_in.png │ │ ├── uid_sso_apps.png │ │ ├── uid_tool_collection_app.png │ │ └── uid_workspace.png │ └── zitadel │ ├── README.md │ └── images │ ├── project_create_overview.png │ ├── project_method.png │ ├── project_name_type.png │ ├── project_overview.png │ ├── project_secrets.png │ ├── project_uris.png │ ├── projects_create.png │ └── projects_overview.png ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── question.yml └── workflows │ └── publish.yml ├── .gitignore ├── Dockerfile ├── LICENCE ├── README.md ├── crowdin.yml ├── css └── style.css ├── docker-compose.yml ├── locales ├── da │ ├── email.json │ ├── kiosk.json │ └── print.json ├── de │ ├── email.json │ ├── kiosk.json │ └── print.json ├── en │ ├── email.json │ ├── kiosk.json │ └── print.json ├── es │ ├── email.json │ ├── kiosk.json │ └── print.json ├── fr │ ├── email.json │ ├── kiosk.json │ └── print.json ├── nl │ ├── email.json │ ├── kiosk.json │ └── print.json ├── pl │ ├── email.json │ ├── kiosk.json │ └── print.json └── ru │ ├── email.json │ ├── kiosk.json │ └── print.json ├── middlewares ├── authorization.js └── flashMessage.js ├── modules ├── cache.js ├── config.js ├── info.js ├── jwt.js ├── log.js ├── mail.js ├── oidc.js ├── print.js ├── qr.js ├── translation.js ├── unifi.js └── variables.js ├── package-lock.json ├── package.json ├── public ├── fonts │ ├── Roboto-Bold.ttf │ └── Roboto-Regular.ttf ├── images │ ├── favicon.ico │ ├── icon │ │ ├── logo_192x192.png │ │ ├── logo_256x256.png │ │ └── logo_512x512.png │ ├── kiosk_bg.jpg │ ├── logo.png │ ├── logo.svg │ ├── logo_grayscale.png │ ├── logo_grayscale_dark.png │ └── screenshots │ │ ├── desktop_screenshot_1.png │ │ ├── desktop_screenshot_2.png │ │ ├── desktop_screenshot_3.png │ │ ├── desktop_screenshot_4.png │ │ ├── mobile_screenshot_1.png │ │ ├── mobile_screenshot_2.png │ │ ├── mobile_screenshot_3.png │ │ └── mobile_screenshot_4.png ├── manifest.json └── robots.txt ├── server.js ├── tailwind.config.js ├── template ├── 404.ejs ├── 500.ejs ├── components │ ├── bulk-print.ejs │ ├── details.ejs │ ├── email.ejs │ └── print.ejs ├── email │ └── voucher.ejs ├── kiosk.ejs ├── login.ejs ├── partials │ ├── navigation.ejs │ └── tag.ejs ├── status.ejs └── voucher.ejs └── utils ├── array.js ├── bytes.js ├── cache.js ├── languages.js ├── logo.js ├── size.js ├── status.js ├── time.js └── types.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # PhpStorm 2 | .idea 3 | 4 | # General files 5 | *~ 6 | *.DS_STORE 7 | 8 | # Dependency managers 9 | node_modules/ 10 | npm-debug.log 11 | 12 | # Build files 13 | public/dist/ 14 | 15 | # Local config 16 | .options.json 17 | 18 | # Project files 19 | .docs 20 | .github 21 | .editorconfig 22 | .gitignore 23 | crowdin.yml 24 | docker-compose.yml 25 | Dockerfile 26 | README.md 27 | -------------------------------------------------------------------------------- /.docs/images/desktop_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/desktop_0.png -------------------------------------------------------------------------------- /.docs/images/desktop_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/desktop_1.png -------------------------------------------------------------------------------- /.docs/images/desktop_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/desktop_2.png -------------------------------------------------------------------------------- /.docs/images/desktop_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/desktop_3.png -------------------------------------------------------------------------------- /.docs/images/email_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/email_example.png -------------------------------------------------------------------------------- /.docs/images/escpos_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/escpos_example.jpg -------------------------------------------------------------------------------- /.docs/images/kiosk_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/kiosk_example.png -------------------------------------------------------------------------------- /.docs/images/mobile_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/mobile_0.png -------------------------------------------------------------------------------- /.docs/images/mobile_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/mobile_1.png -------------------------------------------------------------------------------- /.docs/images/mobile_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/mobile_2.png -------------------------------------------------------------------------------- /.docs/images/mobile_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/mobile_3.png -------------------------------------------------------------------------------- /.docs/images/pdf_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/images/pdf_example.png -------------------------------------------------------------------------------- /.docs/oidc/authentik/README.md: -------------------------------------------------------------------------------- 1 | # Authentik OIDC 2 | 3 | ## 1. Authentik Application Configuration 4 | 5 | ### Step 1: Log in to the Authentik Admin Interface 6 | 7 | 1. Access the Authentik admin interface (e.g., `https://auth.example.com/if/admin`). 8 | 2. Log in with your admin credentials. 9 | 10 | ### Step 2: Create a Provider 11 | 12 | 1. Go to **Providers** in the left-hand menu. 13 | 2. Click **Create Provider**. 14 | 3. Choose **OAuth2/OpenID Connect Provider** as the provider type. 15 | 4. Set the following fields: 16 | - **Name**: `unifi-voucher-provider`. 17 | - **Authentication flow**: `default-authentication-flow`. 18 | - **Authorization flow**: `default-provider-authorization-implicit-consent`. 19 | - **Client Type**: Select `confidential`, A client secret will be generated. 20 | 5. Set the **Redirect URI** to match your UniFi Voucher Site’s callback URL (e.g., `https://voucher.example.com/oidc/callback`). 21 | 6. Click **Submit**. 22 | 23 | ![Create Provider 1](images/create_provider_1.png) 24 | ![Create Provider 2](images/create_provider_2.png) 25 | ![Create Provider 3](images/create_provider_3.png) 26 | 27 | > After saving, note down the **Client ID** and **Client Secret** generated for this provider. You’ll need it when configuring your UniFi Voucher Site. 28 | 29 | ### Step 3: Create a New Application 30 | 31 | 1. Go to **Applications** in the left-hand menu. 32 | 2. Click **Create Application**. 33 | 3. Fill in the following fields: 34 | - **Name**: `UniFi Voucher` (You can choose any relevant name). 35 | - **Slug**: This is a URL-friendly identifier (e.g., `unifi-voucher`). 36 | - **Provider**: Select the provider you created in step 2 (e.g., `unifi-voucher-provider`). 37 | 4. Click **Submit** to save. 38 | 39 | ![Create Application](images/create_application.png) 40 | 41 | --- 42 | 43 | ## 2. UniFi Voucher Site Configuration 44 | 45 | Now, configure your UniFi Voucher Site to use the Authentik client. 46 | 47 | 1. In your UniFi Voucher Site configuration, set `AUTH_OIDC_ENABLED` to `true`. 48 | 2. Set the `AUTH_OIDC_CLIENT_ID` as configured in Authentik (found in the Authentik provider configuration). 49 | 3. Provide the `AUTH_OIDC_CLIENT_SECRET` (found in the Authentik provider configuration). 50 | 4. Provide the `AUTH_OIDC_ISSUER_BASE_URL` from your Authentik provider. 51 | - You can find this under **Providers > unifi-voucher-provider > OpenID Configuration URL** in Authentik. 52 | 5. Provide the `AUTH_OIDC_APP_BASE_URL` from your UniFi Voucher Site instance (e.g., `https://voucher.example.com`). 53 | 6. Restart the container after these changes 54 | 55 | --- 56 | 57 | ## 3. Testing and Troubleshooting 58 | 59 | 1. Test the login flow from your UniFi Voucher Site. Ensure it redirects to Authentik for authentication. 60 | 2. After logging in, the user should be redirected back to the voucher site with the appropriate tokens. 61 | 62 | ### Common Issues 63 | 64 | - **Invalid Redirect URI**: Ensure the callback URI matches what is configured in Authentik. 65 | - **Client Secret Errors**: Double-check the client secret in both Authentik and your UniFi configuration. 66 | -------------------------------------------------------------------------------- /.docs/oidc/authentik/images/create_application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/authentik/images/create_application.png -------------------------------------------------------------------------------- /.docs/oidc/authentik/images/create_provider_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/authentik/images/create_provider_1.png -------------------------------------------------------------------------------- /.docs/oidc/authentik/images/create_provider_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/authentik/images/create_provider_2.png -------------------------------------------------------------------------------- /.docs/oidc/authentik/images/create_provider_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/authentik/images/create_provider_3.png -------------------------------------------------------------------------------- /.docs/oidc/keycloak/README.md: -------------------------------------------------------------------------------- 1 | # Keycloak OIDC 2 | 3 | ## 1. Keycloak Client Configuration 4 | 5 | ### Step 1: Log in to the Keycloak Admin Console 6 | 7 | 1. Go to your Keycloak admin console (e.g., `https://auth.example.com/`). 8 | 2. Log in with your admin credentials. 9 | 10 | ### Step 2: Create a New Realm (Optional) 11 | 12 | If you don't already have a realm: 13 | 14 | 1. Click **Add Realm** in the left-hand menu. 15 | 2. Name your realm (e.g., `unifi-voucher`). 16 | 3. Save the realm. 17 | 18 | ![Create Realm](images/create_realm.png) 19 | 20 | ### Step 3: Create a Client 21 | 22 | 1. Inside your realm, go to **Clients** in the left-hand menu. 23 | 2. Click **Create**. 24 | 3. Fill in the following fields: 25 | - **Client ID**: `unifi-voucher-site` (You can choose any name relevant to your UniFi Voucher Site). 26 | - **Client Protocol**: `openid-connect`. 27 | - Click **Save**. 28 | 29 | ![Create Client](images/create_client.png) 30 | 31 | ### Step 4: Configure the Client 32 | 33 | You’ll see various tabs for configuring the client. Set the following fields: 34 | 35 | 1. Go to the **Settings** tab. 36 | 2. Set **Access Type** to `confidential`. 37 | 3. Ensure **Standard Flow Enabled** is set to `ON`. 38 | 4. Set **Valid Redirect URIs** to your UniFi voucher callback URL (e.g., `https://voucher.example.com/oidc/callback`). 39 | 5. Click **Save**. 40 | 41 | 6. After saving, go to the **Credentials** tab to get the **Client Secret**. This secret will be used by your UniFi Voucher Site when authenticating as a confidential client. 42 | 43 | ![Client Settings 1](images/client_settings_1.png) 44 | ![Client Settings 2](images/client_settings_2.png) 45 | ![Client Secret](images/client_secret.png) 46 | 47 | --- 48 | 49 | ## 2. UniFi Voucher Site Configuration 50 | 51 | Now, configure your UniFi Voucher Site to use the Keycloak client. 52 | 53 | 1. In your UniFi Voucher Site configuration, set `AUTH_OIDC_ENABLED` to `true`. 54 | 2. Set the `AUTH_OIDC_CLIENT_ID` as configured in Keycloak (e.g., `unifi-voucher-site`). 55 | 3. Provide the `AUTH_OIDC_CLIENT_SECRET` (found in the Credentials tab in Keycloak). 56 | 4. Provide the `AUTH_OIDC_ISSUER_BASE_URL` from your Keycloak server (e.g., `https://auth.example.com/realms/{realm}/.well-known/openid-configuration`). 57 | 5. Provide the `AUTH_OIDC_APP_BASE_URL` from your UniFi Voucher Site instance (e.g., `https://voucher.example.com`). 58 | 6. Restart the container after these changes 59 | 60 | --- 61 | 62 | ## 3. Testing and Troubleshooting 63 | 64 | 1. Test the login flow from your UniFi Voucher Site. Ensure it redirects to Keycloak for authentication. 65 | 2. After logging in, the user should be redirected back to the voucher site with the appropriate tokens. 66 | 67 | ### Common Issues 68 | 69 | - **Invalid Redirect URI**: Ensure the callback URI matches what is configured in Keycloak. 70 | - **Client Secret Errors**: Double-check the client secret in both Keycloak and your UniFi configuration. 71 | -------------------------------------------------------------------------------- /.docs/oidc/keycloak/images/client_secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/keycloak/images/client_secret.png -------------------------------------------------------------------------------- /.docs/oidc/keycloak/images/client_settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/keycloak/images/client_settings_1.png -------------------------------------------------------------------------------- /.docs/oidc/keycloak/images/client_settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/keycloak/images/client_settings_2.png -------------------------------------------------------------------------------- /.docs/oidc/keycloak/images/create_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/keycloak/images/create_client.png -------------------------------------------------------------------------------- /.docs/oidc/keycloak/images/create_realm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/keycloak/images/create_realm.png -------------------------------------------------------------------------------- /.docs/oidc/uid/README.md: -------------------------------------------------------------------------------- 1 | # UniFi Identity Enterprise (UID) 2 | 3 | ## 1. UID Application Configuration 4 | 5 | ### Step 1: Log in to the Identity Enterprise Workspace 6 | 7 | 1. Access the UID workspace (e.g., `https://your-site.ui.com`). 8 | 2. Log in with your credentials. 9 | 10 | ![UID Workspace](images/uid_workspace.png) 11 | 12 | ### Step 2: Create a new application 13 | 14 | 1. Select the `Manager Portal`. You will be prompted to verify with MFA. 15 | 2. Select `SSO Apps` in the left-hand menu. 16 | 3. Press the **Plus** button in the top right-hand corner. 17 | 4. Select `Add Custom App` 18 | 5. Select `OIDC` from the menu 19 | 6. Fill in the details for your application. 20 | The required fields needed are `Initiate Sign-In URI` and `Sign-In Redirect URI`. 21 | 22 | Initiate Sign-In URI - (e.g., `https://voucher.example.com`) 23 | Sign-In Redirect URI - (e.g., `https://voucher.example.com/oidc/callback`) 24 | 25 | 7. Press Add. You will now be presented with your Tool Collection for the app. Copy your `Client ID`, `Client Secret` and the value form your `Well Known Config Endpoint`. 26 | 8. Press Done. You can now assign users or groups to the application. The setup has been completed UID side. 27 | 28 | ![UID Manager Portal](images/uid_manager_portal.png) 29 | ![UID SSO Apps](images/uid_sso_apps.png) 30 | ![UID Custom App](images/uid_custom_app.png) 31 | ![UID OIDC](images/uid_oidc.png) 32 | ![UID Add OIDC App](images/uid_add_oidc_app.png) 33 | ![UID Tool Collection App](images/uid_tool_collection_app.png) 34 | 35 | --- 36 | 37 | ## 2. UniFi Voucher Site Configuration 38 | 39 | Now, configure your UniFi Voucher Site to use the UID client. 40 | 41 | 1. In your UniFi Voucher Site configuration, set `AUTH_OIDC_ENABLED` to `true`. 42 | 2. Set the `AUTH_OIDC_CLIENT_ID` as found within the UID Application. 43 | 3. Provide the `AUTH_OIDC_CLIENT_SECRET` as found within the UID Application. 44 | 4. Provide the `AUTH_OIDC_ISSUER_BASE_URL` from your UID domain (e.g., `https://your-site.ui.com/gw/idp/api/v1/public/oauth/your-secret-token/.well-known/openid-configuration`). 45 | 5. Provide the `AUTH_OIDC_APP_BASE_URL` from your UniFi Voucher Site instance (e.g., `https://voucher.example.com`). 46 | 6. Restart the container after these changes 47 | 48 | --- 49 | 50 | ## 3. Testing and Troubleshooting 51 | 52 | **From `UID Workspace`** 53 | 54 | Navigate to the Applications section and select your application. This will launch the application. If you followed the steps correctly you should be able to access the voucher site without needing to authenticate. 55 | 56 | ![UID App Launcher](images/uid_app_launcher.png) 57 | ![UID Demo Sign-In](images/uid_demo_sign_in.png) 58 | ![UID OIDC Sign-In Success](images/uid_oidc_sign_in_success.png) 59 | 60 | **External Sign In from outside of UID** 61 | 62 | > Notice: You will only be prompted for UID sign-in if you have not signed in within your predefined sign in policy in UID. 63 | 64 | Access your application via the `Initiate Sign-In URI` this will prompt a new window to sign in to UID. Once you sign in you will be redirected back to your application. 65 | 66 | ![UID Sign-In](images/uid_sign_in.png) 67 | ![UID Demo Sign-In](images/uid_demo_sign_in.png) 68 | ![UID OIDC Sign-In Success](images/uid_oidc_sign_in_success.png) 69 | 70 | That's it you now have OIDC setup and can sign in to your application! 71 | -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_add_oidc_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_add_oidc_app.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_app_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_app_launcher.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_custom_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_custom_app.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_demo_sign_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_demo_sign_in.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_manager_portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_manager_portal.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_oidc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_oidc.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_oidc_sign_in_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_oidc_sign_in_success.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_sign_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_sign_in.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_sso_apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_sso_apps.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_tool_collection_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_tool_collection_app.png -------------------------------------------------------------------------------- /.docs/oidc/uid/images/uid_workspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/uid/images/uid_workspace.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/README.md: -------------------------------------------------------------------------------- 1 | # ZITADEL 2 | 3 | ## 1. ZITADEL Project and Application Configuration 4 | 5 | ### Step 1: Log in to the ZITADEL Console 6 | 7 | 1. Go to your ZITADEL admin console (e.g., `https://auth.example.com`). 8 | 2. Log in with your admin credentials. 9 | 10 | ### Step 2: Create a Project 11 | 12 | 1. In the ZITADEL console, go to **Projects**. 13 | 2. Click **Create New Project**. 14 | 3. Name your project (e.g., `UniFi Voucher Site`). 15 | 4. Click **Create**. 16 | 17 | ![Projects Overview](images/projects_overview.png) 18 | ![Projects Create](images/projects_create.png) 19 | 20 | ### Step 3: Create an Application (OAuth2 Client) 21 | 22 | Now, create an application under the project you just created. 23 | 24 | 1. Select your project (e.g., `UniFi Voucher Site`). 25 | 2. Under Applications click **Add**. 26 | 3. Fill in the following fields and click **Continue**: 27 | - **Name**: `Production`. 28 | - **Type**: `Web`. 29 | 4. Select **Code** and click **Continue**. 30 | 5. Fill in the following fields and click **Continue**: 31 | - **Login Redirect URIs**: Enter the URL of your UniFi Voucher callback (e.g., `https://voucher.example.com/oidc/callback`). 32 | - **Logout Redirect URIs**: Enter the root URL of your UniFi Voucher instance (e.g., `https://voucher.example.com`). 33 | 6. Click **Create** to save the application. 34 | 7. Save the Client ID and Client Secret shown within the popup and click **Close** 35 | 36 | ![Project Overview](images/project_overview.png) 37 | ![Project Name & Type](images/project_name_type.png) 38 | ![Project Method](images/project_method.png) 39 | ![Project Uris](images/project_uris.png) 40 | ![Project Create Overview](images/project_create_overview.png) 41 | ![Project Secrets](images/project_secrets.png) 42 | 43 | --- 44 | 45 | ## 2. UniFi Voucher Site Configuration 46 | 47 | Now, configure your UniFi Voucher Site to use the ZITADEL client. 48 | 49 | 1. In your UniFi Voucher Site configuration, set `AUTH_OIDC_ENABLED` to `true`. 50 | 2. Set the `AUTH_OIDC_CLIENT_ID` as the ClientId found within the ZITADEL Popup. 51 | 3. Provide the `AUTH_OIDC_CLIENT_SECRET` as the ClientSecret found within the ZITADEL Popup. 52 | 4. Provide the `AUTH_OIDC_ISSUER_BASE_URL` from your Keycloak server (e.g., `https://auth.example.com/.well-known/openid-configuration`). 53 | 5. Provide the `AUTH_OIDC_APP_BASE_URL` from your UniFi Voucher Site instance (e.g., `https://voucher.example.com`). 54 | 6. Restart the container after these changes 55 | 56 | --- 57 | 58 | ## 3. Testing and Troubleshooting 59 | 60 | 1. Test the login flow from your UniFi Voucher Site. It should redirect users to ZITADEL for authentication. 61 | 2. After logging in, users should be redirected back to the voucher site with tokens from ZITADEL. 62 | 63 | ### Common Issues 64 | 65 | - **Invalid Redirect URI**: Ensure the callback URI matches what is configured in ZITADEL. 66 | - **Client Secret Errors**: Ensure that the client secret in both ZITADEL and your UniFi configuration match. 67 | -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/project_create_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/project_create_overview.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/project_method.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/project_method.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/project_name_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/project_name_type.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/project_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/project_overview.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/project_secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/project_secrets.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/project_uris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/project_uris.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/projects_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/projects_create.png -------------------------------------------------------------------------------- /.docs/oidc/zitadel/images/projects_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/.docs/oidc/zitadel/images/projects_overview.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # Set default charset with unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | # JS overrides 12 | [*.js] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | # SCSS and JSON overrides 17 | [*.{scss,json}] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Report an issue with UniFi Voucher Site 2 | description: Report an issue with UniFi Voucher Site. 3 | labels: ["bug"] 4 | assignees: 5 | - glenndehaan 6 | body: 7 | - type: textarea 8 | validations: 9 | required: true 10 | attributes: 11 | label: The problem 12 | description: >- 13 | Describe the issue you are experiencing here, to communicate to the 14 | maintainers. Tell us what you were trying to do and what happened. 15 | 16 | Provide a clear and concise description of what the problem is. 17 | If you have screenshots please place them here. 18 | - type: markdown 19 | attributes: 20 | value: | 21 | ## Environment 22 | - type: input 23 | id: version 24 | validations: 25 | required: true 26 | attributes: 27 | label: What version of UniFi Voucher Site has the issue? 28 | placeholder: x.x.x 29 | description: > 30 | Use the docker image tag, Home Assistant version or the version displayed within the logs. 31 | - type: input 32 | attributes: 33 | label: What was the last working version of UniFi Voucher Site? 34 | placeholder: x.x.x 35 | description: > 36 | If known, otherwise leave blank. 37 | - type: dropdown 38 | validations: 39 | required: true 40 | attributes: 41 | label: What type of installation are you running? 42 | description: > 43 | This refers to the installation guide you followed here: https://github.com/glenndehaan/unifi-voucher-site#installation 44 | options: 45 | - Docker 46 | - Home Assistant Add-on 47 | - Development Version (Not Recommended) 48 | 49 | - type: markdown 50 | attributes: 51 | value: | 52 | # Details 53 | - type: textarea 54 | attributes: 55 | label: Anything in the logs that might be useful for us? 56 | description: For example, error messages or stack traces. 57 | render: Text 58 | - type: textarea 59 | attributes: 60 | label: Additional information 61 | description: > 62 | If you have any additional information for us, use the field below. 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Propose a feature for UniFi Voucher Site 2 | description: Propose a feature for UniFi Voucher Site. 3 | labels: ["enhancement"] 4 | assignees: 5 | - glenndehaan 6 | body: 7 | - type: textarea 8 | validations: 9 | required: true 10 | attributes: 11 | label: The feature 12 | description: >- 13 | Describe your question here. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Need help with UniFi Voucher Site 2 | description: Need help with UniFi Voucher Site. 3 | labels: ["question"] 4 | assignees: 5 | - glenndehaan 6 | body: 7 | - type: textarea 8 | validations: 9 | required: true 10 | attributes: 11 | label: The question 12 | description: >- 13 | Describe your feature request here. 14 | Tell us what would like to see developed next. 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # When the workflow succeeds the package will be published to docker hub 2 | 3 | name: Publish Docker Image 4 | 5 | on: 6 | push: 7 | branches: 8 | - 'master' 9 | tags: 10 | - '*' 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v3 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | - name: Login to DockerHub 23 | uses: docker/login-action@v3 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | - name: Build and push 28 | if: startsWith(github.ref, 'refs/tags/') 29 | uses: docker/build-push-action@v5 30 | with: 31 | context: . 32 | platforms: linux/amd64,linux/arm64 33 | push: true 34 | tags: glenndehaan/unifi-voucher-site:${{ github.ref_name }} 35 | build-args: | 36 | GIT_TAG=${{ github.ref_name }} 37 | - name: Build and push (latest) 38 | if: github.ref == 'refs/heads/master' 39 | uses: docker/build-push-action@v5 40 | with: 41 | context: . 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: glenndehaan/unifi-voucher-site:latest 45 | build-args: | 46 | GIT_TAG=master 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PhpStorm 2 | .idea 3 | 4 | # General files 5 | *~ 6 | *.DS_STORE 7 | 8 | # Dependency managers 9 | node_modules/ 10 | npm-debug.log 11 | 12 | # Build files 13 | public/dist/ 14 | 15 | # Local config 16 | .options.json 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #===================================================== 2 | # Build Stage (1/2) 3 | #===================================================== 4 | 5 | # 6 | # Define OS 7 | # 8 | FROM alpine:3.21 AS dependencies 9 | 10 | # 11 | # Basic OS management 12 | # 13 | 14 | # Install packages 15 | RUN apk add --no-cache nodejs npm 16 | 17 | # 18 | # Require app 19 | # 20 | 21 | # Create app directory 22 | WORKDIR /app 23 | 24 | # Bundle package.json and package-lock.json 25 | COPY ./package.json ./package-lock.json ./ 26 | 27 | # Install dependencies 28 | RUN npm ci --only=production && npm cache clean --force 29 | 30 | #===================================================== 31 | # Build Stage (2/2) 32 | #===================================================== 33 | 34 | # 35 | # Define OS 36 | # 37 | FROM alpine:3.21 AS css 38 | 39 | # 40 | # Basic OS management 41 | # 42 | 43 | # Install packages 44 | RUN apk add --no-cache nodejs npm 45 | 46 | # 47 | # Require app 48 | # 49 | 50 | # Create app directory 51 | WORKDIR /app 52 | 53 | # Bundle package.json and package-lock.json 54 | COPY ./package.json ./package-lock.json ./ 55 | 56 | # Install dependencies 57 | RUN npm ci && npm cache clean --force 58 | 59 | # Bundle application source 60 | COPY . . 61 | 62 | # Create a production build 63 | RUN npm run build 64 | 65 | #===================================================== 66 | # Image Stage 67 | #===================================================== 68 | 69 | # 70 | # Define OS 71 | # 72 | FROM alpine:3.21 73 | 74 | # 75 | # Basic OS management 76 | # 77 | 78 | # Install packages 79 | RUN apk add --no-cache dumb-init nodejs icu-data-full 80 | 81 | # 82 | # Require app 83 | # 84 | 85 | # Create app directory 86 | WORKDIR /app 87 | 88 | # 89 | # Setup app 90 | # 91 | 92 | # Expose app 93 | EXPOSE 3000 94 | 95 | # Set node env 96 | ENV NODE_ENV=production 97 | 98 | # Setup healthcheck 99 | HEALTHCHECK --interval=10s --timeout=3s \ 100 | CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/_health || exit 1 101 | 102 | # Run app 103 | CMD ["dumb-init", "node", "/app/server.js"] 104 | 105 | # 106 | # Bundle app 107 | # 108 | 109 | # Bundle from build image 110 | COPY --from=dependencies /app/node_modules ./node_modules 111 | COPY --from=css /app/public/dist ./public/dist 112 | COPY . . 113 | 114 | # 115 | # Set build 116 | # 117 | RUN echo -n `date '+%d/%m/%Y, %H:%M'` > /etc/unifi_voucher_site_build 118 | ARG GIT_TAG 119 | ENV GIT_TAG=$GIT_TAG 120 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Glenn de Haan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /locales/en/*.json 3 | translation: /locales/%two_letters_code%/%original_file_name% 4 | 5 | "pull_request_assignees": [ 6 | "glenndehaan" 7 | ] 8 | 9 | "pull_request_reviewers": [ 10 | "glenndehaan" 11 | ] 12 | 13 | "pull_request_labels": [ 14 | "crowdin", 15 | "translations" 16 | ] 17 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin '@tailwindcss/forms'; 3 | 4 | /* 5 | The default border color has changed to `currentColor` in Tailwind CSS v4, 6 | so we've added these compatibility styles to make sure everything still 7 | looks the same as it did with Tailwind CSS v3. 8 | 9 | If we ever want to remove these styles, we need to add an explicit border 10 | color utility to any element that depends on these defaults. 11 | */ 12 | /* 13 | The default cursor has changed to `default` in Tailwind CSS v4, 14 | so we've added these compatibility styles to make sure everything still 15 | looks the same as it did with Tailwind CSS v3. 16 | 17 | If we ever want to remove these styles, we need to add an cursor 18 | utility to any element that depends on these defaults. 19 | */ 20 | @layer base { 21 | *, 22 | ::after, 23 | ::before, 24 | ::backdrop, 25 | ::file-selector-button { 26 | border-color: var(--color-gray-200, currentColor); 27 | } 28 | 29 | button:not(:disabled), 30 | [role="button"]:not(:disabled) { 31 | cursor: pointer; 32 | } 33 | } 34 | 35 | .timer-progress { 36 | position: fixed; 37 | top: 0; 38 | left: 0; 39 | right: 0; 40 | overflow: hidden; 41 | height: 6px; 42 | z-index: 100; 43 | } 44 | 45 | .timer-bar { 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | bottom: 0; 50 | width: 100%; 51 | transform-origin: right; 52 | } 53 | 54 | #timer-bar { 55 | transform: translateX(-100%); 56 | } 57 | 58 | @keyframes countdown { 59 | from { transform: translateX(-100%); } 60 | to { transform: translateX(0); } 61 | } 62 | 63 | .animate-countdown { 64 | animation: countdown 60s linear forwards; 65 | will-change: transform; 66 | } 67 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | ports: 5 | - "3000:3000" 6 | environment: 7 | UNIFI_IP: '192.168.1.1' 8 | UNIFI_PORT: 443 9 | UNIFI_USERNAME: 'admin' 10 | UNIFI_PASSWORD: 'password' 11 | UNIFI_SITE_ID: 'default' 12 | UNIFI_SSID: '' 13 | UNIFI_SSID_PASSWORD: '' 14 | AUTH_INTERNAL_ENABLED: 'true' 15 | AUTH_INTERNAL_PASSWORD: '0000' 16 | AUTH_INTERNAL_BEARER_TOKEN: '00000000-0000-0000-0000-000000000000' 17 | AUTH_OIDC_ENABLED: 'false' 18 | AUTH_OIDC_ISSUER_BASE_URL: '' 19 | AUTH_OIDC_APP_BASE_URL: '' 20 | AUTH_OIDC_CLIENT_ID: '' 21 | AUTH_OIDC_CLIENT_SECRET: '' 22 | AUTH_DISABLE: 'false' 23 | VOUCHER_TYPES: '480,1,,,;' 24 | VOUCHER_CUSTOM: 'true' 25 | SERVICE_WEB: 'true' 26 | SERVICE_API: 'false' 27 | PRINTERS: '' 28 | SMTP_FROM: '' 29 | SMTP_HOST: '' 30 | SMTP_PORT: '' 31 | SMTP_SECURE: '' 32 | SMTP_USERNAME: '' 33 | SMTP_PASSWORD: '' 34 | KIOSK_ENABLED: 'false' 35 | KIOSK_VOUCHER_TYPE: '480,1,,,' 36 | LOG_LEVEL: 'info' 37 | TRANSLATION_DEBUG: 'false' 38 | -------------------------------------------------------------------------------- /locales/da/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Rabatkupon Kode", 3 | "preHeader": "Din Wi-Fi-rabatkode", 4 | "greeting": "Hej der", 5 | "intro": "Nogen har genereret en WiFi-rabatkode, brug venligst denne kode ved tilslutning", 6 | "connect": "Opret forbindelse til", 7 | "password": "Adgangskode", 8 | "or": "eller", 9 | "scan": "Scan for at forbinde", 10 | "details": "Værdikupon Detaljer", 11 | "type": "Type", 12 | "multiUse": "Multi-brug", 13 | "singleUse": "Enkelt brug", 14 | "duration": "Varighed", 15 | "dataLimit": "Grænse For Data", 16 | "downloadLimit": "Download Grænse", 17 | "uploadLimit": "Upload Grænse", 18 | "poweredBy": "Drevet af" 19 | } 20 | -------------------------------------------------------------------------------- /locales/da/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Rabatkupon", 3 | "generate": "Generer WiFi Rabatkupon", 4 | "generating": "Genererer Rabatkupon", 5 | "use": "Brug denne kode ved tilslutning", 6 | "connect": "Opret forbindelse til", 7 | "password": "Adgangskode", 8 | "or": "eller", 9 | "scan": "Scan for at forbinde", 10 | "email": "Email Voucher", 11 | "optional": "valgfri", 12 | "enterEmail": "Indtast din e-mail adresse", 13 | "send": "Send Rabatkupon", 14 | "sending": "Sender E-Mail", 15 | "sent": "E-mail Sendt", 16 | "back": "Tilbage" 17 | } 18 | -------------------------------------------------------------------------------- /locales/da/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Rabatkupon Kode", 3 | "connect": "Opret forbindelse til", 4 | "password": "Adgangskode", 5 | "or": "eller", 6 | "scan": "Scan for at forbinde", 7 | "details": "Værdikupon Detaljer", 8 | "type": "Type", 9 | "multiUse": "Multi-brug", 10 | "singleUse": "Enkelt brug", 11 | "duration": "Varighed", 12 | "dataLimit": "Grænse For Data", 13 | "downloadLimit": "Download Grænse", 14 | "uploadLimit": "Upload Grænse" 15 | } 16 | -------------------------------------------------------------------------------- /locales/de/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Voucher Code", 3 | "preHeader": "Ihr WiFi Voucher Code", 4 | "greeting": "Hallo", 5 | "intro": "Dies ist Ihr WiFi Voucher Code. Bitte verwenden Sie diesen Code, um sich mit dem WiFi zu verbinden", 6 | "connect": "Verbinden mit", 7 | "password": "Passwort", 8 | "or": "oder", 9 | "scan": "Scan zum Verbinden", 10 | "details": "Voucher Details", 11 | "type": "Typ", 12 | "multiUse": "Mehrfach benutzbar", 13 | "singleUse": "Einmalig benutzbar", 14 | "duration": "Dauer", 15 | "dataLimit": "Datenlimit", 16 | "downloadLimit": "Download-Limit", 17 | "uploadLimit": "Upload-Limit", 18 | "poweredBy": "Powered by" 19 | } 20 | -------------------------------------------------------------------------------- /locales/de/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi-Gutschein", 3 | "generate": "WiFi Gutschein generieren", 4 | "generating": "Gutschein generieren", 5 | "use": "Diesen Code beim Verbinden verwenden", 6 | "connect": "Verbinden mit", 7 | "password": "Passwort", 8 | "or": "oder", 9 | "scan": "Scan zum Verbinden", 10 | "email": "Email Voucher", 11 | "optional": "optional", 12 | "enterEmail": "Geben Sie Ihre E-Mail-Adresse ein", 13 | "send": "Gutschein senden", 14 | "sending": "E-Mail wird gesendet", 15 | "sent": "E-Mail gesendet", 16 | "back": "Zurück" 17 | } 18 | -------------------------------------------------------------------------------- /locales/de/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Voucher Code", 3 | "connect": "Verbinden mit", 4 | "password": "Passwort", 5 | "or": "oder", 6 | "scan": "Scan zum Verbinden", 7 | "details": "Voucher Details", 8 | "type": "Typ", 9 | "multiUse": "Mehrfach benutzbar", 10 | "singleUse": "Einmalig benutzbar", 11 | "duration": "Dauer", 12 | "dataLimit": "Datenlimit", 13 | "downloadLimit": "Download-Limit", 14 | "uploadLimit": "Upload-Limit" 15 | } 16 | -------------------------------------------------------------------------------- /locales/en/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Voucher Code", 3 | "preHeader": "Your WiFi Voucher Code", 4 | "greeting": "Hi there", 5 | "intro": "Someone generated a WiFi Voucher, please use this code when connecting", 6 | "connect": "Connect to", 7 | "password": "Password", 8 | "or": "or", 9 | "scan": "Scan to connect", 10 | "details": "Voucher Details", 11 | "type": "Type", 12 | "multiUse": "Multi-use", 13 | "singleUse": "Single-use", 14 | "duration": "Duration", 15 | "dataLimit": "Data Limit", 16 | "downloadLimit": "Download Limit", 17 | "uploadLimit": "Upload Limit", 18 | "poweredBy": "Powered by" 19 | } 20 | -------------------------------------------------------------------------------- /locales/en/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Voucher", 3 | "generate": "Generate WiFi Voucher", 4 | "generating": "Generating Voucher", 5 | "use": "Use this code when connecting", 6 | "connect": "Connect to", 7 | "password": "Password", 8 | "or": "or", 9 | "scan": "Scan to connect", 10 | "email": "Email Voucher", 11 | "optional": "optional", 12 | "enterEmail": "Enter your email address", 13 | "send": "Send Voucher", 14 | "sending": "Sending Email", 15 | "sent": "Email Sent", 16 | "back": "Back" 17 | } 18 | -------------------------------------------------------------------------------- /locales/en/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Voucher Code", 3 | "connect": "Connect to", 4 | "password": "Password", 5 | "or": "or", 6 | "scan": "Scan to connect", 7 | "details": "Voucher Details", 8 | "type": "Type", 9 | "multiUse": "Multi-use", 10 | "singleUse": "Single-use", 11 | "duration": "Duration", 12 | "dataLimit": "Data Limit", 13 | "downloadLimit": "Download Limit", 14 | "uploadLimit": "Upload Limit" 15 | } 16 | -------------------------------------------------------------------------------- /locales/es/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Código del vale WiFi", 3 | "preHeader": "Tu código de cupón WiFi", 4 | "greeting": "Hola", 5 | "intro": "Alguien generó un cupón WiFi, por favor use este código al conectar", 6 | "connect": "Conectar a", 7 | "password": "Contraseña", 8 | "or": "o", 9 | "scan": "Escanear para conectar", 10 | "details": "Detalles del cupón", 11 | "type": "Tipo", 12 | "multiUse": "Multi-uso", 13 | "singleUse": "Un solo uso", 14 | "duration": "Duración", 15 | "dataLimit": "Límite de datos", 16 | "downloadLimit": "Límite de descarga", 17 | "uploadLimit": "Límite de subida", 18 | "poweredBy": "Desarrollado por" 19 | } 20 | -------------------------------------------------------------------------------- /locales/es/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Cupón WiFi", 3 | "generate": "Generar cupón WiFi", 4 | "generating": "Generando cupón", 5 | "use": "Usar este código al conectar", 6 | "connect": "Conectar a", 7 | "password": "Contraseña", 8 | "or": "o", 9 | "scan": "Escanear para conectar", 10 | "email": "Email Voucher", 11 | "optional": "opcional", 12 | "enterEmail": "Introduzca su dirección de correo", 13 | "send": "Enviar cupón", 14 | "sending": "Enviando Email", 15 | "sent": "Correo enviado", 16 | "back": "Atrás" 17 | } 18 | -------------------------------------------------------------------------------- /locales/es/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Código del vale WiFi", 3 | "connect": "Conectar a", 4 | "password": "Contraseña", 5 | "or": "o", 6 | "scan": "Escanear para conectar", 7 | "details": "Detalles del cupón", 8 | "type": "Tipo", 9 | "multiUse": "Multi-uso", 10 | "singleUse": "Un solo uso", 11 | "duration": "Duración", 12 | "dataLimit": "Límite de datos", 13 | "downloadLimit": "Límite de descarga", 14 | "uploadLimit": "Límite de subida" 15 | } 16 | -------------------------------------------------------------------------------- /locales/fr/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Code de connexion WiFi", 3 | "preHeader": "Votre code de bon de réduction WiFi", 4 | "greeting": "Bonjour", 5 | "intro": "Quelqu'un a généré un bon de réduction WiFi, veuillez utiliser ce code lors de la connexion", 6 | "connect": "Se connecter à", 7 | "password": "Mot de passe", 8 | "or": "ou", 9 | "scan": "Scanner pour se connecter", 10 | "details": "Détails du bon d'achat", 11 | "type": "Type de texte", 12 | "multiUse": "Multi-usages", 13 | "singleUse": "Utilisation unique", 14 | "duration": "Durée", 15 | "dataLimit": "Limite de données", 16 | "downloadLimit": "Limite de téléchargement", 17 | "uploadLimit": "Limite de téléchargement", 18 | "poweredBy": "Propulsé par" 19 | } 20 | -------------------------------------------------------------------------------- /locales/fr/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Coupon WiFi", 3 | "generate": "Générer un bon de réduction WiFi", 4 | "generating": "Génération du bon de réduction", 5 | "use": "Utilisez ce code lors de la connexion", 6 | "connect": "Se connecter à", 7 | "password": "Mot de passe", 8 | "or": "ou", 9 | "scan": "Scanner pour se connecter", 10 | "email": "Email Voucher", 11 | "optional": "optionnel", 12 | "enterEmail": "Entrez votre adresse e-mail", 13 | "send": "Envoyer un bon de réduction", 14 | "sending": "Envoi de l'e-mail", 15 | "sent": "Courriel envoyé", 16 | "back": "Précédent" 17 | } 18 | -------------------------------------------------------------------------------- /locales/fr/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Code de connexion WiFi", 3 | "connect": "Se connecter à", 4 | "password": "Mot de passe", 5 | "or": "ou", 6 | "scan": "Scanner pour se connecter", 7 | "details": "Détails du bon d'achat", 8 | "type": "Type de texte", 9 | "multiUse": "Multi-usages", 10 | "singleUse": "Utilisation unique", 11 | "duration": "Durée", 12 | "dataLimit": "Limite de données", 13 | "downloadLimit": "Limite de téléchargement", 14 | "uploadLimit": "Limite de téléchargement" 15 | } 16 | -------------------------------------------------------------------------------- /locales/nl/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Waardebon Code", 3 | "preHeader": "Uw WiFi Waardebon Code", 4 | "greeting": "Hallo daar", 5 | "intro": "Iemand heeft een WiFi Waardebon gegenereerd, gebruik deze code bij het verbinden", 6 | "connect": "Verbind met", 7 | "password": "Wachtwoord", 8 | "or": "of", 9 | "scan": "Scan om te verbinden", 10 | "details": "Waardebon Details", 11 | "type": "Type", 12 | "multiUse": "Meervoudig gebruik", 13 | "singleUse": "Enkelvoudig gebruik", 14 | "duration": "Duur", 15 | "dataLimit": "Data Limiet", 16 | "downloadLimit": "Download limiet", 17 | "uploadLimit": "Upload limiet", 18 | "poweredBy": "Mogelijk gemaakt door" 19 | } 20 | -------------------------------------------------------------------------------- /locales/nl/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Waardebon", 3 | "generate": "WiFi Waardebon Genereren", 4 | "generating": "Waardebon Aanmaken", 5 | "use": "Gebruik deze code bij verbinding maken", 6 | "connect": "Verbind met", 7 | "password": "Wachtwoord", 8 | "or": "of", 9 | "scan": "Scan om te verbinden", 10 | "email": "Email Voucher", 11 | "optional": "optioneel", 12 | "enterEmail": "Voer uw e-mailadres in", 13 | "send": "Waardebon Verzenden", 14 | "sending": "E-mail verzenden", 15 | "sent": "E-mail verzonden", 16 | "back": "Terug" 17 | } 18 | -------------------------------------------------------------------------------- /locales/nl/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WiFi Waardebon", 3 | "connect": "Verbind met", 4 | "password": "Wachtwoord", 5 | "or": "of", 6 | "scan": "Scan om te verbinden", 7 | "details": "Waardebon Details", 8 | "type": "Type", 9 | "multiUse": "Meervoudig gebruik", 10 | "singleUse": "Enkelvoudig gebruik", 11 | "duration": "Duur", 12 | "dataLimit": "Data Limiet", 13 | "downloadLimit": "Download limiet", 14 | "uploadLimit": "Upload limiet" 15 | } 16 | -------------------------------------------------------------------------------- /locales/pl/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Voucher WiFi", 3 | "preHeader": "Twój Voucher WiFi", 4 | "greeting": "Cześć", 5 | "intro": "Ktoś wygenerował kupon WiFi, użyj tego kodu podczas łączenia", 6 | "connect": "Połącz z", 7 | "password": "Hasło", 8 | "or": "lub", 9 | "scan": "Zeskanuj aby połączyć", 10 | "details": "Szczegóły bonu", 11 | "type": "Typ", 12 | "multiUse": "Wielokrotne użycie", 13 | "singleUse": "Jednorazowe użycie", 14 | "duration": "Czas trwania", 15 | "dataLimit": "Limit danych", 16 | "downloadLimit": "Limit pobierania", 17 | "uploadLimit": "Limit wysyłania", 18 | "poweredBy": "Wspierane przez" 19 | } 20 | -------------------------------------------------------------------------------- /locales/pl/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Voucher WiFi", 3 | "generate": "Generuj kupon WiFi", 4 | "generating": "Generowanie bonu", 5 | "use": "Użyj tego kodu podczas łączenia", 6 | "connect": "Połącz z", 7 | "password": "Hasło", 8 | "or": "lub", 9 | "scan": "Zeskanuj aby połączyć", 10 | "email": "Email Voucher", 11 | "optional": "fakultatywne", 12 | "enterEmail": "Wprowadź swój adres e-mail", 13 | "send": "Wyślij kupon", 14 | "sending": "Wysyłanie wiadomości", 15 | "sent": "E-mail wysłany", 16 | "back": "Powrót" 17 | } 18 | -------------------------------------------------------------------------------- /locales/pl/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Voucher WiFi", 3 | "connect": "Połącz z", 4 | "password": "Hasło", 5 | "or": "lub", 6 | "scan": "Zeskanuj aby połączyć", 7 | "details": "Szczegóły vouchera", 8 | "type": "Typ", 9 | "multiUse": "Wielokrotne użycie", 10 | "singleUse": "Jednorazowe użycie", 11 | "duration": "Czas trwania", 12 | "dataLimit": "Limit danych", 13 | "downloadLimit": "Limit pobierania", 14 | "uploadLimit": "Limit wysyłania" 15 | } 16 | -------------------------------------------------------------------------------- /locales/ru/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Код купона WiFi", 3 | "preHeader": "Ваш код купона WiFi", 4 | "greeting": "Привет,", 5 | "intro": "Кто-то создал купон WiFi, пожалуйста, используйте этот код при подключении", 6 | "connect": "Подключиться к", 7 | "password": "Пароль", 8 | "or": "или", 9 | "scan": "Отсканируйте чтобы подключиться", 10 | "details": "Информация о купоне", 11 | "type": "Тип", 12 | "multiUse": "Многоразовый", 13 | "singleUse": "Одноразовый", 14 | "duration": "Продолжительность", 15 | "dataLimit": "Лимит данных", 16 | "downloadLimit": "Лимит скачивания", 17 | "uploadLimit": "Лимит отдачи", 18 | "poweredBy": "При поддержке" 19 | } 20 | -------------------------------------------------------------------------------- /locales/ru/kiosk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Купон WiFi", 3 | "generate": "Сгенерировать купон WiFi", 4 | "generating": "Создание купона", 5 | "use": "Использовать этот код при подключении", 6 | "connect": "Подключиться к", 7 | "password": "Пароль", 8 | "or": "или", 9 | "scan": "Сканировать для подключения", 10 | "email": "Отправить ваучер по e-mail", 11 | "optional": "опционально", 12 | "enterEmail": "Введите ваш адрес электронной почты", 13 | "send": "Отправить ваучер", 14 | "sending": "Отправка Email", 15 | "sent": "Отправлено E-mail", 16 | "back": "Назад" 17 | } 18 | -------------------------------------------------------------------------------- /locales/ru/print.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Код купона WiFi", 3 | "connect": "Подключиться к", 4 | "password": "Пароль", 5 | "or": "или", 6 | "scan": "Сканировать для подключения", 7 | "details": "Информация о купоне", 8 | "type": "Тип", 9 | "multiUse": "Мульти-использование", 10 | "singleUse": "Одноразовое использование", 11 | "duration": "Продолжительность", 12 | "dataLimit": "Лимит данных", 13 | "downloadLimit": "Лимит скачивания", 14 | "uploadLimit": "Лимит отдачи" 15 | } 16 | -------------------------------------------------------------------------------- /middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const oidc = require('express-openid-connect'); 5 | 6 | /** 7 | * Import own modules 8 | */ 9 | const variables = require('../modules/variables'); 10 | const jwt = require('../modules/jwt'); 11 | 12 | /** 13 | * Verifies if a user is signed in 14 | * 15 | * @type {{web: ((function(*, *, *): Promise)|*), api: ((function(*, *, *): Promise)|*)}} 16 | */ 17 | module.exports = { 18 | /** 19 | * Handle web authentication 20 | * 21 | * @param req 22 | * @param res 23 | * @param next 24 | * @return {Promise} 25 | */ 26 | web: async (req, res, next) => { 27 | let internal = false; 28 | let oidc = false; 29 | 30 | // Continue is authentication is disabled 31 | if(variables.authDisabled) { 32 | next(); 33 | return; 34 | } 35 | 36 | // Check if Internal auth is enabled then verify user status 37 | if(variables.authInternalEnabled) { 38 | // Check if user has an existing authorization cookie 39 | if (req.cookies.authorization) { 40 | // Check if token is correct and valid 41 | try { 42 | const check = jwt.verify(req.cookies.authorization); 43 | 44 | if(check) { 45 | internal = true; 46 | } 47 | } catch (e) {} 48 | } 49 | } 50 | 51 | // Check if OIDC is enabled then verify user status 52 | if(variables.authOidcEnabled) { 53 | oidc = req.oidc.isAuthenticated(); 54 | } 55 | 56 | // Check if user is authorized by a service 57 | if(internal || oidc) { 58 | // Remove req.oidc if user is authenticated internally 59 | if(internal) { 60 | delete req.oidc; 61 | } 62 | 63 | next(); 64 | return; 65 | } 66 | 67 | // Fallback to login page 68 | res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/login`); 69 | }, 70 | 71 | /** 72 | * Handle api authentication 73 | * 74 | * @param req 75 | * @param res 76 | * @param next 77 | * @return {Promise} 78 | */ 79 | api: async (req, res, next) => { 80 | // Check if authentication is enabled 81 | if(!variables.authDisabled) { 82 | // Check if user has sent the authorization header 83 | if (!req.headers.authorization) { 84 | res.status(401).json({ 85 | error: 'Unauthorized', 86 | data: {} 87 | }); 88 | return; 89 | } 90 | 91 | // Check if password is correct 92 | const passwordCheck = req.headers.authorization === `Bearer ${variables.authToken}`; 93 | if (!passwordCheck) { 94 | res.status(403).json({ 95 | error: 'Forbidden', 96 | data: {} 97 | }); 98 | return; 99 | } 100 | } 101 | 102 | next(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /middlewares/flashMessage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves a flash message from a cookie if available 3 | * 4 | * @param req 5 | * @param res 6 | * @param next 7 | */ 8 | module.exports = async (req, res, next) => { 9 | req.flashMessage = { 10 | type: '', 11 | message: '' 12 | }; 13 | 14 | if(req.cookies.flashMessage) { 15 | req.flashMessage = JSON.parse(req.cookies.flashMessage); 16 | res.cookie('flashMessage', '', {httpOnly: true, expires: new Date(0)}) 17 | } 18 | 19 | next(); 20 | } 21 | -------------------------------------------------------------------------------- /modules/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal application cache 3 | * 4 | * @type {{guests: *[], vouchers: *[], updated: number}} 5 | */ 6 | module.exports = { 7 | vouchers: [], 8 | guests: [], 9 | updated: 0 10 | }; 11 | -------------------------------------------------------------------------------- /modules/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base modules 3 | */ 4 | const fs = require('fs'); 5 | 6 | /** 7 | * Get an option from external config (Home Assistant / Local Development) 8 | * 9 | * @param option 10 | * @return {*|null} 11 | */ 12 | module.exports = (option) => { 13 | // Check if Home Assistant config exists 14 | if (fs.existsSync('/data/options.json')) { 15 | const data = JSON.parse(fs.readFileSync('/data/options.json', 'utf-8')); 16 | return typeof data[option] !== 'undefined' ? data[option] : null; 17 | } 18 | 19 | // Check if Local (Development) config exists 20 | if (fs.existsSync(`${__dirname}/../.options.json`)) { 21 | const data = JSON.parse(fs.readFileSync(`${__dirname}/../.options.json`, 'utf-8')); 22 | return typeof data[option] !== 'undefined' ? data[option] : null; 23 | } 24 | 25 | return null; 26 | }; 27 | -------------------------------------------------------------------------------- /modules/info.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const fs = require('fs'); 5 | 6 | /** 7 | * Import own modules 8 | */ 9 | const variables = require('./variables'); 10 | const log = require('./log'); 11 | 12 | /** 13 | * Import own utils 14 | */ 15 | const array = require('../utils/array'); 16 | const logo = require('../utils/logo'); 17 | const types = require('../utils/types'); 18 | const time = require('../utils/time'); 19 | const languages = require('../utils/languages'); 20 | 21 | /** 22 | * Output info to console 23 | */ 24 | module.exports = () => { 25 | /** 26 | * Output logo 27 | */ 28 | logo(); 29 | 30 | /** 31 | * Check for deprecated strings 32 | */ 33 | array.deprecated.forEach((item) => { 34 | if(typeof process.env[item] !== 'undefined') { 35 | log.warn(`[Deprecation] '${item}' has been deprecated! Please remove this item from the environment variables and/or follow migration guides: https://github.com/glenndehaan/unifi-voucher-site#migration-guide`); 36 | } 37 | }); 38 | 39 | /** 40 | * Output build version 41 | */ 42 | log.info(`[Version] Git: ${variables.gitTag} - Build: ${variables.gitBuild}`); 43 | 44 | /** 45 | * Log external config 46 | */ 47 | if (fs.existsSync('/data/options.json')) { 48 | log.info('[Options] Found at /data/options.json'); 49 | } 50 | if (fs.existsSync(`${process.cwd()}/.options.json`)) { 51 | log.info(`[Options] Found at ${process.cwd()}/.options.json`); 52 | } 53 | 54 | /** 55 | * Check for incorrect translation default 56 | */ 57 | if(!Object.keys(languages).includes(variables.translationDefault)) { 58 | log.error(`[Translations] Default language: '${variables.translationDefault}' doesn't exist!`); 59 | } 60 | 61 | /** 62 | * Log service status 63 | */ 64 | log.info(`[Service][Web] ${variables.serviceWeb ? 'Enabled!' : 'Disabled!'}`); 65 | log.info(`[Service][Api] ${variables.serviceApi ? 'Enabled!' : 'Disabled!'}`); 66 | 67 | /** 68 | * Log voucher types 69 | */ 70 | log.info('[Voucher] Loaded the following types:'); 71 | types(variables.voucherTypes).forEach((type, key) => { 72 | log.info(`[Voucher][Type][${key}] ${time(type.expiration)}, ${type.usage === '1' ? 'single-use' : type.usage === '0' ? 'multi-use (unlimited)' : `multi-use (${type.usage}x)`}${typeof type.upload === "undefined" && typeof type.download === "undefined" && typeof type.megabytes === "undefined" ? ', no limits' : `${typeof type.upload !== "undefined" ? `, upload bandwidth limit: ${type.upload} kb/s` : ''}${typeof type.download !== "undefined" ? `, download bandwidth limit: ${type.download} kb/s` : ''}${typeof type.megabytes !== "undefined" ? `, quota limit: ${type.megabytes} mb` : ''}`}`); 73 | }); 74 | log.info(`[Voucher][Custom] ${variables.voucherCustom ? 'Enabled!' : 'Disabled!'}`); 75 | 76 | /** 77 | * Log auth status 78 | */ 79 | log.info(`[Auth] ${variables.authDisabled ? 'Disabled!' : `Enabled! Type: ${variables.authInternalEnabled ? 'Internal' : ''}${variables.authInternalEnabled && variables.authOidcEnabled ? ', ' : ''}${variables.authOidcEnabled ? 'OIDC' : ''}`}`); 80 | 81 | /** 82 | * Check auth services 83 | */ 84 | if(!variables.authDisabled && !variables.authInternalEnabled && !variables.authOidcEnabled) { 85 | log.error(`[Auth] Incorrect Configuration Detected!. Authentication is enabled but all authentication services have been disabled`); 86 | } 87 | 88 | /** 89 | * Verify OIDC configuration 90 | */ 91 | if(variables.authOidcEnabled && (variables.authOidcIssuerBaseUrl === '' || variables.authOidcAppBaseUrl === '' || variables.authOidcClientId === '' || variables.authOidcClientSecret === '')) { 92 | log.error(`[OIDC] Incorrect Configuration Detected!. Verify 'AUTH_OIDC_ISSUER_BASE_URL', 'AUTH_OIDC_APP_BASE_URL', 'AUTH_OIDC_CLIENT_ID' and 'AUTH_OIDC_CLIENT_SECRET' are set! Authentication will be unstable or disabled until issue is resolved!`); 93 | } 94 | 95 | /** 96 | * Log printer status 97 | */ 98 | log.info(`[Printers] ${variables.printers !== '' ? `Enabled! Available: ${variables.printers.split(',').join(', ')}` : 'Disabled!'}`); 99 | 100 | /** 101 | * Log email status 102 | */ 103 | if(variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '') { 104 | log.info(`[Email] Enabled! SMTP Server: ${variables.smtpHost}:${variables.smtpPort}`); 105 | } else { 106 | log.info(`[Email] Disabled!`); 107 | } 108 | 109 | /** 110 | * Log kiosk status 111 | */ 112 | if(variables.kioskEnabled) { 113 | const kioskType = types(variables.kioskVoucherType, true); 114 | log.info('[Kiosk] Enabled!'); 115 | log.info(`[Kiosk][Type] ${time(kioskType.expiration)}, ${kioskType.usage === '1' ? 'single-use' : kioskType.usage === '0' ? 'multi-use (unlimited)' : `multi-use (${kioskType.usage}x)`}${typeof kioskType.upload === "undefined" && typeof kioskType.download === "undefined" && typeof kioskType.megabytes === "undefined" ? ', no limits' : `${typeof kioskType.upload !== "undefined" ? `, upload bandwidth limit: ${kioskType.upload} kb/s` : ''}${typeof kioskType.download !== "undefined" ? `, download bandwidth limit: ${kioskType.download} kb/s` : ''}${typeof kioskType.megabytes !== "undefined" ? `, quota limit: ${kioskType.megabytes} mb` : ''}`}`); 116 | } 117 | 118 | /** 119 | * Log controller 120 | */ 121 | log.info(`[UniFi] Using Controller on: ${variables.unifiIp}:${variables.unifiPort} (Site ID: ${variables.unifiSiteId}${variables.unifiSsid !== '' ? `, SSID: ${variables.unifiSsid}` : ''})`); 122 | 123 | /** 124 | * Check for valid UniFi username 125 | */ 126 | if(variables.unifiUsername.includes('@')) { 127 | log.error('[UniFi] Incorrect username detected! UniFi Cloud credentials are not supported!'); 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /modules/jwt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const crypto = require('crypto'); 5 | const jwt = require('jsonwebtoken'); 6 | 7 | /** 8 | * Import own modules 9 | */ 10 | const log = require('./log'); 11 | 12 | /** 13 | * JWT Settings 14 | * 15 | * @type {{expiresIn: string, secret: string, algorithm: string}} 16 | */ 17 | const settings = { 18 | algorithm: 'HS512', 19 | secret: '', 20 | expiresIn: '24h' 21 | }; 22 | 23 | /** 24 | * Exports the JWT functions 25 | */ 26 | module.exports = { 27 | /** 28 | * Set the JWT secret 29 | */ 30 | init: () => { 31 | settings.secret = crypto.randomBytes(20).toString('hex'); 32 | log.info(`[JWT] Set secret: ${settings.secret}`); 33 | }, 34 | 35 | /** 36 | * Sign a payload and return a JWT token 37 | * 38 | * @param payload 39 | * @return {*} 40 | */ 41 | sign: (payload = {}) => { 42 | return jwt.sign(payload, settings.secret, { 43 | algorithm: settings.algorithm, 44 | expiresIn: settings.expiresIn 45 | }); 46 | }, 47 | 48 | /** 49 | * Verify a JWT token 50 | * 51 | * @param token 52 | * @return {*} 53 | */ 54 | verify: (token) => { 55 | return jwt.verify(token, settings.secret); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /modules/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const log = require('js-logger'); 5 | 6 | /** 7 | * Import own modules 8 | */ 9 | const variables = require('./variables'); 10 | 11 | /** 12 | * Setup logger 13 | */ 14 | const consoleLogger = log.createDefaultHandler({ 15 | formatter: (messages, context) => { 16 | // Get current date, change this to the current timezone, then generate a date-time string 17 | const utcDate = new Date(); 18 | const offset = utcDate.getTimezoneOffset(); 19 | const date = new Date(utcDate.getTime() - (offset * 60 * 1000)); 20 | const dateTimeString = date.toISOString().replace('T', ' ').replace('Z', ''); 21 | 22 | // Prefix each log message with a timestamp and log level 23 | messages.unshift(`${dateTimeString} ${context.level.name}${context.level.name === 'INFO' || context.level.name === 'WARN' ? ' ' : ''}`); 24 | } 25 | }); 26 | 27 | /** 28 | * Log Level converter 29 | */ 30 | const logConvert = (level) => { 31 | switch(level) { 32 | case "error": 33 | return log.ERROR; 34 | case "warn": 35 | return log.WARN; 36 | case "info": 37 | return log.INFO; 38 | case "debug": 39 | return log.DEBUG; 40 | case "trace": 41 | return log.TRACE; 42 | default: 43 | return log.INFO; 44 | } 45 | } 46 | 47 | /** 48 | * Set all logger handlers 49 | */ 50 | log.setHandler((messages, context) => { 51 | consoleLogger(messages, context); 52 | }); 53 | 54 | /** 55 | * Set log level 56 | */ 57 | log.setLevel(logConvert(variables.logLevel)); 58 | 59 | /** 60 | * Export the application logger 61 | */ 62 | module.exports = log; 63 | -------------------------------------------------------------------------------- /modules/mail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import vendor modules 3 | */ 4 | const fs = require('fs'); 5 | const ejs = require('ejs'); 6 | const nodemailer = require('nodemailer'); 7 | 8 | /** 9 | * Import own modules 10 | */ 11 | const variables = require('./variables'); 12 | const log = require('./log'); 13 | const translation = require('./translation'); 14 | const qr = require('./qr'); 15 | 16 | /** 17 | * Import own utils 18 | */ 19 | const time = require('../utils/time'); 20 | const bytes = require('../utils/bytes'); 21 | 22 | /** 23 | * Create nodemailer transport 24 | */ 25 | const transport = nodemailer.createTransport({ 26 | host: variables.smtpHost, 27 | port: parseInt(variables.smtpPort), 28 | secure: variables.smtpSecure, 29 | tls: { 30 | rejectUnauthorized: false // Skip TLS Certificate checks for Self-Hosted systems 31 | }, 32 | auth: { 33 | user: variables.smtpUsername, 34 | pass: variables.smtpPassword 35 | } 36 | }); 37 | 38 | /** 39 | * Mail module functions 40 | */ 41 | module.exports = { 42 | /** 43 | * Sends an email via the nodemailer transport 44 | * 45 | * @param to 46 | * @param voucher 47 | * @param language 48 | * @return {Promise} 49 | */ 50 | send: (to, voucher, language = 'en') => { 51 | return new Promise(async (resolve, reject) => { 52 | // Create new translator 53 | const t = translation('email', language); 54 | 55 | // Attempt to send mail via SMTP transport 56 | const result = await transport.sendMail({ 57 | from: variables.smtpFrom, 58 | to: to, 59 | subject: t('title'), 60 | text: `${t('greeting')},\n\n${t('intro')}:\n\n${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`, 61 | html: ejs.render(fs.readFileSync(`${__dirname}/../template/email/voucher.ejs`, 'utf-8'), { 62 | t, 63 | voucher, 64 | unifiSsid: variables.unifiSsid, 65 | unifiSsidPassword: variables.unifiSsidPassword, 66 | qr: await qr(), 67 | timeConvert: time, 68 | bytesConvert: bytes 69 | }) 70 | }).catch((e) => { 71 | log.error(`[Mail] Error when sending mail`); 72 | log.error(e); 73 | reject(`[Mail] ${e.message}`); 74 | }); 75 | 76 | // Check if the email was sent successfully 77 | if(result) { 78 | log.info(`[Mail] Sent to: ${to}`); 79 | resolve(true); 80 | } 81 | }); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /modules/oidc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const crypto = require('crypto'); 5 | const oidc = require('express-openid-connect'); 6 | 7 | /** 8 | * Import own modules 9 | */ 10 | const variables = require('./variables'); 11 | const log = require('./log'); 12 | 13 | /** 14 | * OIDC Settings 15 | */ 16 | const settings = { 17 | issuerBaseURL: variables.authOidcIssuerBaseUrl, 18 | baseURL: variables.authOidcAppBaseUrl, 19 | clientID: variables.authOidcClientId, 20 | clientSecret: variables.authOidcClientSecret, 21 | secret: '', 22 | idpLogout: true, 23 | authRequired: false, 24 | attemptSilentLogin: true, 25 | authorizationParams: { 26 | response_type: 'code', 27 | response_mode: 'query', 28 | scope: 'openid profile email' 29 | }, 30 | routes: { 31 | callback: '/oidc/callback', 32 | login: '/oidc/login', 33 | logout: '/oidc/logout' 34 | } 35 | }; 36 | 37 | /** 38 | * Exports the OIDC functions 39 | */ 40 | module.exports = { 41 | /** 42 | * Set the OIDC secret & setup OIDC middleware 43 | * 44 | * @param app 45 | */ 46 | init: (app) => { 47 | settings.secret = crypto.randomBytes(20).toString('hex'); 48 | log.info(`[OIDC] Set secret: ${settings.secret}`); 49 | app.use(oidc.auth(settings)); 50 | log.info(`[OIDC] Issuer: ${settings.issuerBaseURL}, Client: ${settings.clientID}`); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /modules/print.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const PDFDocument = require('pdfkit'); 5 | const ThermalPrinter = require('node-thermal-printer').printer; 6 | const PrinterTypes = require('node-thermal-printer').types; 7 | 8 | /** 9 | * Import own modules 10 | */ 11 | const variables = require('./variables'); 12 | const log = require('./log'); 13 | const qr = require('./qr'); 14 | const translation = require('./translation'); 15 | 16 | /** 17 | * Import own utils 18 | */ 19 | const time = require('../utils/time'); 20 | const bytes = require('../utils/bytes'); 21 | const size = require('../utils/size'); 22 | 23 | /** 24 | * Exports the printer module 25 | */ 26 | module.exports = { 27 | /** 28 | * Generates a voucher as a PDF 29 | * 30 | * @param content 31 | * @param language 32 | * @param multiPage 33 | * @return {Promise} 34 | */ 35 | pdf: (content, language, multiPage= false) => { 36 | return new Promise(async (resolve) => { 37 | // Create new translator 38 | const t = translation('print', language); 39 | 40 | // Set vouchers based on multiPage parameter 41 | let vouchers = []; 42 | if(multiPage) { 43 | vouchers = [...content]; 44 | } else { 45 | vouchers = [content]; 46 | } 47 | 48 | const doc = new PDFDocument({ 49 | bufferPages: true, 50 | size: [226.77165354330398, size(vouchers[0])], 51 | margins : { 52 | top: 20, 53 | bottom: 20, 54 | left: 20, 55 | right: 20 56 | } 57 | }); 58 | 59 | // Utilize custom font for custom characters 60 | doc.font(__dirname + '/../public/fonts/Roboto-Regular.ttf'); 61 | doc.font(__dirname + '/../public/fonts/Roboto-Bold.ttf'); 62 | 63 | const buffers = []; 64 | doc.on('data', buffers.push.bind(buffers)); 65 | doc.on('end', () => { 66 | log.info('[Printer] PDF generation completed!'); 67 | resolve(buffers); 68 | }); 69 | 70 | for(let item = 0; item < vouchers.length; item++) { 71 | if(item > 0) { 72 | doc.addPage({ 73 | size: [226.77165354330398, size(vouchers[item])], 74 | margins : { 75 | top: 20, 76 | bottom: 20, 77 | left: 20, 78 | right: 20 79 | } 80 | }); 81 | 82 | doc.moveDown(1); 83 | } 84 | 85 | doc.image('public/images/logo_grayscale_dark.png', 75, 15, { 86 | fit: [75, 75], 87 | align: 'center', 88 | valign: 'center' 89 | }); 90 | doc.moveDown(6); 91 | 92 | doc.font('Roboto-Bold') 93 | .fontSize(20) 94 | .text(`${t('title')}`, { 95 | align: 'center' 96 | }); 97 | doc.font('Roboto-Bold') 98 | .fontSize(15) 99 | .text(`${vouchers[item].code.slice(0, 5)}-${vouchers[item].code.slice(5)}`, { 100 | align: 'center' 101 | }); 102 | 103 | doc.moveDown(2); 104 | 105 | if (variables.unifiSsid !== '') { 106 | doc.font('Roboto-Regular') 107 | .fontSize(10) 108 | .text(`${t('connect')}: `, { 109 | continued: true 110 | }); 111 | doc.font('Roboto-Bold') 112 | .fontSize(10) 113 | .text(variables.unifiSsid, { 114 | continued: true 115 | }); 116 | 117 | if (variables.unifiSsidPassword !== '') { 118 | doc.font('Roboto-Regular') 119 | .fontSize(10) 120 | .text(`,`); 121 | doc.font('Roboto-Regular') 122 | .fontSize(10) 123 | .text(`${t('password')}: `, { 124 | continued: true 125 | }); 126 | doc.font('Roboto-Bold') 127 | .fontSize(10) 128 | .text(variables.unifiSsidPassword, { 129 | continued: true 130 | }); 131 | doc.font('Roboto-Regular') 132 | .fontSize(10) 133 | .text(` ${t('or')},`); 134 | } else { 135 | doc.font('Roboto-Regular') 136 | .fontSize(10) 137 | .text(` ${t('or')},`); 138 | } 139 | 140 | doc.font('Roboto-Regular') 141 | .fontSize(10) 142 | .text(`${t('scan')}:`); 143 | 144 | doc.image(await qr(), 75, variables.unifiSsidPassword !== '' ? 255 : 205, { 145 | fit: [75, 75], 146 | align: 'center', 147 | valign: 'center' 148 | }); 149 | doc.moveDown(6); 150 | 151 | // Check if we need to move the text down extra or not depending on if large SSIDs or Passwords are used 152 | if(variables.unifiSsidPassword !== '' && (variables.unifiSsidPassword.length < 16 || variables.unifiSsidPassword.length < 32)) { 153 | doc.moveDown(2); 154 | } 155 | 156 | doc.moveDown(2); 157 | } 158 | 159 | doc.font('Roboto-Bold') 160 | .fontSize(12) 161 | .text(`${t('details')}`); 162 | 163 | doc.font('Roboto-Bold') 164 | .fontSize(10) 165 | .text(`------------------------------------------`); 166 | 167 | doc.font('Roboto-Bold') 168 | .fontSize(10) 169 | .text(`${t('type')}: `, { 170 | continued: true 171 | }); 172 | doc.font('Roboto-Regular') 173 | .fontSize(10) 174 | .text(vouchers[item].quota === 1 ? t('singleUse') : vouchers[item].quota === 0 ? t('multiUse') : t('multiUse')); 175 | 176 | doc.font('Roboto-Bold') 177 | .fontSize(10) 178 | .text(`${t('duration')}: `, { 179 | continued: true 180 | }); 181 | doc.font('Roboto-Regular') 182 | .fontSize(10) 183 | .text(time(vouchers[item].duration)); 184 | 185 | if (vouchers[item].qos_usage_quota) { 186 | doc.font('Roboto-Bold') 187 | .fontSize(10) 188 | .text(`${t('dataLimit')}: `, { 189 | continued: true 190 | }); 191 | doc.font('Roboto-Regular') 192 | .fontSize(10) 193 | .text(`${bytes(vouchers[item].qos_usage_quota, 2)}`); 194 | } 195 | 196 | if (vouchers[item].qos_rate_max_down) { 197 | doc.font('Roboto-Bold') 198 | .fontSize(10) 199 | .text(`${t('downloadLimit')}: `, { 200 | continued: true 201 | }); 202 | doc.font('Roboto-Regular') 203 | .fontSize(10) 204 | .text(`${bytes(vouchers[item].qos_rate_max_down, 1, true)}`); 205 | } 206 | 207 | if (vouchers[item].qos_rate_max_up) { 208 | doc.font('Roboto-Bold') 209 | .fontSize(10) 210 | .text(`${t('uploadLimit')}: `, { 211 | continued: true 212 | }); 213 | doc.font('Roboto-Regular') 214 | .fontSize(10) 215 | .text(`${bytes(vouchers[item].qos_rate_max_up, 1, true)}`); 216 | } 217 | } 218 | 219 | doc.end(); 220 | }); 221 | }, 222 | 223 | /** 224 | * Sends a print job to an ESC/POS compatible network printer 225 | * 226 | * @param voucher 227 | * @param language 228 | * @param ip 229 | * @return {Promise} 230 | */ 231 | escpos: (voucher, language, ip) => { 232 | return new Promise(async (resolve, reject) => { 233 | // Create new translator 234 | const t = translation('print', language); 235 | 236 | const printer = new ThermalPrinter({ 237 | type: PrinterTypes.EPSON, 238 | interface: `tcp://${ip}` 239 | }); 240 | 241 | const status = await printer.isPrinterConnected(); 242 | 243 | if(!status) { 244 | reject('Unable to connect to printer!'); 245 | return; 246 | } 247 | 248 | printer.setTypeFontB(); 249 | printer.alignCenter(); 250 | printer.newLine(); 251 | await printer.printImage(`${process.cwd()}/public/images/logo_grayscale_dark.png`); 252 | printer.newLine(); 253 | 254 | printer.alignCenter(); 255 | printer.newLine(); 256 | printer.setTextSize(2, 2); 257 | printer.println(`${t('title')}`); 258 | printer.setTextSize(1, 1); 259 | printer.println(`${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`); 260 | printer.setTextNormal(); 261 | 262 | if(variables.unifiSsid) { 263 | printer.newLine(); 264 | printer.newLine(); 265 | printer.newLine(); 266 | 267 | printer.alignLeft(); 268 | printer.print(`${t('connect')}: `); 269 | printer.setTypeFontB(); 270 | printer.setTextSize(1, 1); 271 | printer.print(variables.unifiSsid); 272 | printer.setTextNormal(); 273 | if(variables.unifiSsidPassword) { 274 | printer.print(','); 275 | printer.newLine(); 276 | printer.print(`${t('password')}: `); 277 | printer.setTypeFontB(); 278 | printer.setTextSize(1, 1); 279 | printer.print(variables.unifiSsidPassword); 280 | printer.setTextNormal(); 281 | printer.print(` ${t('or')},`); 282 | printer.newLine(); 283 | } else { 284 | printer.print(` ${t('or')},`); 285 | printer.newLine(); 286 | } 287 | printer.println(`${t('scan')}:`); 288 | printer.alignCenter(); 289 | await printer.printImageBuffer(await qr(true)); 290 | } 291 | 292 | printer.newLine(); 293 | printer.newLine(); 294 | 295 | printer.alignLeft(); 296 | printer.setTypeFontB(); 297 | printer.setTextSize(1, 1); 298 | printer.println(`${t('details')}`); 299 | printer.setTextNormal(); 300 | printer.drawLine(); 301 | 302 | printer.setTextDoubleHeight(); 303 | printer.invert(true); 304 | printer.print(`${t('type')}:`); 305 | printer.invert(false); 306 | printer.print(voucher.quota === 1 ? ` ${t('singleUse')}` : voucher.quota === 0 ? ` ${t('multiUse')}` : ` ${t('multiUse')}`); 307 | printer.newLine(); 308 | 309 | printer.setTextDoubleHeight(); 310 | printer.invert(true); 311 | printer.print(`${t('duration')}:`); 312 | printer.invert(false); 313 | printer.print(` ${time(voucher.duration)}`); 314 | printer.newLine(); 315 | 316 | if(voucher.qos_usage_quota) { 317 | printer.setTextDoubleHeight(); 318 | printer.invert(true); 319 | printer.print(`${t('dataLimit')}:`); 320 | printer.invert(false); 321 | printer.print(` ${bytes(voucher.qos_usage_quota, 2)}`); 322 | printer.newLine(); 323 | } 324 | 325 | if(voucher.qos_rate_max_down) { 326 | printer.setTextDoubleHeight(); 327 | printer.invert(true); 328 | printer.print(`${t('downloadLimit')}:`); 329 | printer.invert(false); 330 | printer.print(` ${bytes(voucher.qos_rate_max_down, 1, true)}`); 331 | printer.newLine(); 332 | } 333 | 334 | if(voucher.qos_rate_max_up) { 335 | printer.setTextDoubleHeight(); 336 | printer.invert(true); 337 | printer.print(`${t('uploadLimit')}:`); 338 | printer.invert(false); 339 | printer.print(` ${bytes(voucher.qos_rate_max_up, 1, true)}`); 340 | printer.newLine(); 341 | } 342 | 343 | printer.newLine(); 344 | printer.newLine(); 345 | printer.newLine(); 346 | printer.newLine(); 347 | printer.cut(); 348 | printer.beep(2, 2); 349 | 350 | try { 351 | await printer.execute(); 352 | log.info('[Printer] Data send to printer!'); 353 | 354 | // Ensure cheap printers have cleared the buffer before allowing new actions 355 | setTimeout(() => { 356 | resolve(true); 357 | }, 1500); 358 | } catch (error) { 359 | reject(error); 360 | } 361 | }); 362 | } 363 | }; 364 | -------------------------------------------------------------------------------- /modules/qr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import vendor modules 3 | */ 4 | const QRCode = require('qrcode'); 5 | 6 | /** 7 | * Import own modules 8 | */ 9 | const log = require('./log'); 10 | const variables = require('./variables'); 11 | 12 | /** 13 | * Generates a QR code from the UniFi SSID (Scan to Connect) 14 | * 15 | * @param buffer 16 | * @return {Promise} 17 | */ 18 | module.exports = (buffer = false) => { 19 | // Define QR Content based on Wi-Fi Security Standard 20 | const qrText = variables.unifiSsidPassword !== '' ? `WIFI:S:${variables.unifiSsid};T:WPA;P:${variables.unifiSsidPassword};;` : `WIFI:S:${variables.unifiSsid};;`; 21 | 22 | return new Promise((resolve) => { 23 | if(!buffer) { 24 | QRCode.toDataURL(qrText, { version: 6, errorCorrectionLevel: 'Q' }, (err, url) => { 25 | if(err) { 26 | log.error(`[Qr] Error while generating code!`); 27 | log.error(err); 28 | } 29 | 30 | resolve(url); 31 | }); 32 | } else { 33 | QRCode.toBuffer(qrText, { version: 6, errorCorrectionLevel: 'Q' }, (err, buffer) => { 34 | if(err) { 35 | log.error(`[Qr] Error while generating code!`); 36 | log.error(err); 37 | } 38 | 39 | resolve(buffer); 40 | }); 41 | } 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /modules/translation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const fs = require('fs'); 5 | 6 | /** 7 | * Import own modules 8 | */ 9 | const log = require('./log'); 10 | const variables = require('./variables'); 11 | 12 | /** 13 | * Translation returns translator function 14 | * 15 | * @param module 16 | * @param language 17 | * @param fallback 18 | * @return {(function(key: string): (string))} 19 | */ 20 | module.exports = (module, language = 'en', fallback = 'en') => { 21 | // Prevent users from escaping the filesystem 22 | if(!new RegExp(/^[a-zA-Z]*$/).test(language)) { 23 | log.error(`[Translation] Detected path escalation! Forcing fallback and skipping user input...`); 24 | language = fallback; 25 | } 26 | 27 | // Check if translation file exists 28 | if(!fs.existsSync(`${__dirname}/../locales/${language}/${module}.json`)) { 29 | log.warn(`[Translation] Missing translation file: ${__dirname}/../locales/${language}/${module}.json`); 30 | language = fallback; 31 | log.warn(`[Translation] Using fallback: ${__dirname}/../locales/${language}/${module}.json`); 32 | } 33 | 34 | // Get translation file 35 | const translations = JSON.parse(fs.readFileSync(`${__dirname}/../locales/${language}/${module}.json`, 'utf-8')); 36 | 37 | // Return translate function 38 | return (key) => { 39 | // Check if key exists within translation file 40 | if(typeof translations[key] === 'undefined') { 41 | log.warn(`[Translation][${language}] Missing for key: ${key}`); 42 | return `%${key}%`; 43 | } 44 | 45 | // Check if debugging is enabled. If enabled only return key 46 | return variables.translationDebug ? `t('${key}')` : translations[key]; 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /modules/unifi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import vendor modules 3 | */ 4 | const unifi = require('node-unifi'); 5 | 6 | /** 7 | * Import own modules 8 | */ 9 | const variables = require('./variables'); 10 | const log = require('./log'); 11 | 12 | /** 13 | * UniFi Settings 14 | */ 15 | const settings = { 16 | ip: variables.unifiIp, 17 | port: variables.unifiPort, 18 | username: variables.unifiUsername, 19 | password: variables.unifiPassword, 20 | siteID: variables.unifiSiteId 21 | }; 22 | 23 | /** 24 | * Controller session 25 | */ 26 | let controller = null; 27 | 28 | /** 29 | * Start a UniFi controller reusable session 30 | * 31 | * @return {Promise} 32 | */ 33 | const startSession = () => { 34 | return new Promise((resolve, reject) => { 35 | // Check if we have a current session already 36 | if(controller !== null) { 37 | resolve(); 38 | return; 39 | } 40 | 41 | if(settings.username.includes('@')) { 42 | reject('[UniFi] Incorrect username detected! UniFi Cloud credentials are not supported!'); 43 | return; 44 | } 45 | 46 | // Create new UniFi controller object 47 | controller = new unifi.Controller({ 48 | host: settings.ip, 49 | port: settings.port, 50 | site: settings.siteID, 51 | sslverify: false 52 | }); 53 | 54 | // Login to UniFi Controller 55 | controller.login(settings.username, settings.password).then(() => { 56 | log.debug('[UniFi] Login successful!'); 57 | resolve(); 58 | }).catch((e) => { 59 | // Something went wrong so clear the current controller so a user can retry 60 | controller = null; 61 | log.error('[UniFi] Error while logging in!'); 62 | log.debug(e); 63 | reject('[UniFi] Error while logging in!'); 64 | }); 65 | }); 66 | } 67 | 68 | /** 69 | * UniFi module functions 70 | * 71 | * @type {{create: (function(*, number=, null=, boolean=): Promise<*>), remove: (function(*, boolean=): Promise<*>), list: (function(boolean=): Promise<*>), guests: (function(boolean=): Promise<*>)}} 72 | */ 73 | const unifiModule = { 74 | /** 75 | * Creates a new UniFi Voucher 76 | * 77 | * @param type 78 | * @param amount 79 | * @param note 80 | * @param retry 81 | * @return {Promise} 82 | */ 83 | create: (type, amount = 1, note = null, retry = true) => { 84 | return new Promise((resolve, reject) => { 85 | startSession().then(() => { 86 | controller.createVouchers(type.expiration, amount, parseInt(type.usage), note, typeof type.upload !== "undefined" ? type.upload : null, typeof type.download !== "undefined" ? type.download : null, typeof type.megabytes !== "undefined" ? type.megabytes : null).then((voucher_data) => { 87 | if(amount > 1) { 88 | log.info(`[UniFi] Created ${amount} vouchers`); 89 | resolve(true); 90 | } else { 91 | controller.getVouchers(voucher_data[0].create_time).then((voucher_data_complete) => { 92 | const voucher = `${[voucher_data_complete[0].code.slice(0, 5), '-', voucher_data_complete[0].code.slice(5)].join('')}`; 93 | log.info(`[UniFi] Created voucher with code: ${voucher}`); 94 | resolve(voucher); 95 | }).catch((e) => { 96 | log.error('[UniFi] Error while getting voucher!'); 97 | log.debug(e); 98 | reject('[UniFi] Error while getting voucher!'); 99 | }); 100 | } 101 | }).catch((e) => { 102 | log.error('[UniFi] Error while creating voucher!'); 103 | log.debug(e); 104 | 105 | // Check if token expired, if true attempt login then try again 106 | if (e.response) { 107 | if(e.response.status === 401 && retry) { 108 | log.info('[UniFi] Attempting re-authentication & retry...'); 109 | 110 | controller = null; 111 | unifiModule.create(type, amount, note, false).then((e) => { 112 | resolve(e); 113 | }).catch((e) => { 114 | reject(e); 115 | }); 116 | } else { 117 | // Something else went wrong lets clear the current controller so a user can retry 118 | log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); 119 | controller = null; 120 | reject('[UniFi] Error while creating voucher!'); 121 | } 122 | } else { 123 | // Something else went wrong lets clear the current controller so a user can retry 124 | log.error('[UniFi] Unexpected cleanup controller...'); 125 | controller = null; 126 | reject('[UniFi] Error while creating voucher!'); 127 | } 128 | }); 129 | }).catch((e) => { 130 | reject(e); 131 | }); 132 | }); 133 | }, 134 | 135 | /** 136 | * Removes a UniFi Voucher 137 | * 138 | * @param id 139 | * @param retry 140 | * @return {Promise} 141 | */ 142 | remove: (id, retry = true) => { 143 | return new Promise((resolve, reject) => { 144 | startSession().then(() => { 145 | controller.revokeVoucher(id).then(() => { 146 | resolve(true); 147 | }).catch((e) => { 148 | log.error('[UniFi] Error while removing voucher!'); 149 | log.debug(e); 150 | 151 | // Check if token expired, if true attempt login then try again 152 | if (e.response) { 153 | if(e.response.status === 401 && retry) { 154 | log.info('[UniFi] Attempting re-authentication & retry...'); 155 | 156 | controller = null; 157 | unifiModule.remove(id, false).then((e) => { 158 | resolve(e); 159 | }).catch((e) => { 160 | reject(e); 161 | }); 162 | } else { 163 | // Something else went wrong lets clear the current controller so a user can retry 164 | log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); 165 | controller = null; 166 | reject('[UniFi] Error while removing voucher!'); 167 | } 168 | } else { 169 | // Something else went wrong lets clear the current controller so a user can retry 170 | log.error('[UniFi] Unexpected cleanup controller...'); 171 | controller = null; 172 | reject('[UniFi] Error while removing voucher!'); 173 | } 174 | }); 175 | }).catch((e) => { 176 | reject(e); 177 | }); 178 | }); 179 | }, 180 | 181 | /** 182 | * Returns a list with all UniFi Vouchers 183 | * 184 | * @param retry 185 | * @return {Promise} 186 | */ 187 | list: (retry = true) => { 188 | return new Promise((resolve, reject) => { 189 | startSession().then(() => { 190 | controller.getVouchers().then((vouchers) => { 191 | log.info(`[UniFi] Found ${vouchers.length} voucher(s)`); 192 | resolve(vouchers); 193 | }).catch((e) => { 194 | log.error('[UniFi] Error while getting vouchers!'); 195 | log.debug(e); 196 | 197 | // Check if token expired, if true attempt login then try again 198 | if (e.response) { 199 | if(e.response.status === 401 && retry) { 200 | log.info('[UniFi] Attempting re-authentication & retry...'); 201 | 202 | controller = null; 203 | unifiModule.list(false).then((e) => { 204 | resolve(e); 205 | }).catch((e) => { 206 | reject(e); 207 | }); 208 | } else { 209 | // Something else went wrong lets clear the current controller so a user can retry 210 | log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); 211 | controller = null; 212 | reject('[UniFi] Error while getting vouchers!'); 213 | } 214 | } else { 215 | // Something else went wrong lets clear the current controller so a user can retry 216 | log.error('[UniFi] Unexpected cleanup controller...'); 217 | controller = null; 218 | reject('[UniFi] Error while getting vouchers!'); 219 | } 220 | }); 221 | }).catch((e) => { 222 | reject(e); 223 | }); 224 | }); 225 | }, 226 | 227 | /** 228 | * Returns a list with all UniFi Guests 229 | * 230 | * @param retry 231 | * @return {Promise} 232 | */ 233 | guests: (retry = true) => { 234 | return new Promise((resolve, reject) => { 235 | startSession().then(() => { 236 | controller.getGuests().then((guests) => { 237 | log.info(`[UniFi] Found ${guests.length} guest(s)`); 238 | resolve(guests); 239 | }).catch((e) => { 240 | log.error('[UniFi] Error while getting guests!'); 241 | log.debug(e); 242 | 243 | // Check if token expired, if true attempt login then try again 244 | if (e.response) { 245 | if(e.response.status === 401 && retry) { 246 | log.info('[UniFi] Attempting re-authentication & retry...'); 247 | 248 | controller = null; 249 | unifiModule.guests(false).then((e) => { 250 | resolve(e); 251 | }).catch((e) => { 252 | reject(e); 253 | }); 254 | } else { 255 | // Something else went wrong lets clear the current controller so a user can retry 256 | log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); 257 | controller = null; 258 | reject('[UniFi] Error while getting guests!'); 259 | } 260 | } else { 261 | // Something else went wrong lets clear the current controller so a user can retry 262 | log.error('[UniFi] Unexpected cleanup controller...'); 263 | controller = null; 264 | reject('[UniFi] Error while getting guests!'); 265 | } 266 | }); 267 | }).catch((e) => { 268 | reject(e); 269 | }); 270 | }); 271 | } 272 | } 273 | 274 | /** 275 | * Exports the UniFi module functions 276 | */ 277 | module.exports = unifiModule; 278 | -------------------------------------------------------------------------------- /modules/variables.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import base packages 3 | */ 4 | const fs = require('fs'); 5 | 6 | /** 7 | * Import own modules 8 | */ 9 | const config = require('./config'); 10 | 11 | /** 12 | * Exports all global variables used within the application 13 | */ 14 | module.exports = { 15 | unifiIp: config('unifi_ip') || process.env.UNIFI_IP || '192.168.1.1', 16 | unifiPort: config('unifi_port') || process.env.UNIFI_PORT || 443, 17 | unifiUsername: config('unifi_username') || process.env.UNIFI_USERNAME || 'admin', 18 | unifiPassword: config('unifi_password') || process.env.UNIFI_PASSWORD || 'password', 19 | unifiSiteId: config('unifi_site_id') || process.env.UNIFI_SITE_ID || 'default', 20 | unifiSsid: config('unifi_ssid') || process.env.UNIFI_SSID || '', 21 | unifiSsidPassword: config('unifi_ssid_password') || process.env.UNIFI_SSID_PASSWORD || '', 22 | voucherTypes: config('voucher_types') || process.env.VOUCHER_TYPES || '480,1,,,;', 23 | voucherCustom: config('voucher_custom') !== null ? config('voucher_custom') : process.env.VOUCHER_CUSTOM ? process.env.VOUCHER_CUSTOM !== 'false' : true, 24 | serviceWeb: config('service_web') !== null ? config('service_web') : process.env.SERVICE_WEB ? process.env.SERVICE_WEB !== 'false' : true, 25 | serviceApi: config('service_api') || (process.env.SERVICE_API === 'true') || false, 26 | authInternalEnabled: config('auth_internal_enabled') !== null ? config('auth_internal_enabled') : process.env.AUTH_INTERNAL_ENABLED ? process.env.AUTH_INTERNAL_ENABLED !== 'false' : true, 27 | authInternalPassword: config('auth_internal_password') || process.env.AUTH_INTERNAL_PASSWORD || '0000', 28 | authToken: config('auth_internal_bearer_token') || process.env.AUTH_INTERNAL_BEARER_TOKEN || '00000000-0000-0000-0000-000000000000', 29 | authOidcEnabled: config('auth_oidc_enabled') || (process.env.AUTH_OIDC_ENABLED === 'true') || false, 30 | authOidcIssuerBaseUrl: config('auth_oidc_issuer_base_url') || process.env.AUTH_OIDC_ISSUER_BASE_URL || '', 31 | authOidcAppBaseUrl: config('auth_oidc_app_base_url') || process.env.AUTH_OIDC_APP_BASE_URL || '', 32 | authOidcClientId: config('auth_oidc_client_id') || process.env.AUTH_OIDC_CLIENT_ID || '', 33 | authOidcClientSecret: config('auth_oidc_client_secret') || process.env.AUTH_OIDC_CLIENT_SECRET || '', 34 | authDisabled: config('auth_disable') || (process.env.AUTH_DISABLE === 'true') || false, 35 | printers: config('printers') || process.env.PRINTERS || '', 36 | smtpFrom: config('smtp_from') || process.env.SMTP_FROM || '', 37 | smtpHost: config('smtp_host') || process.env.SMTP_HOST || '', 38 | smtpPort: config('smtp_port') || process.env.SMTP_PORT || 25, 39 | smtpSecure: config('smtp_secure') || (process.env.SMTP_SECURE === 'true') || false, 40 | smtpUsername: config('smtp_username') || process.env.SMTP_USERNAME || '', 41 | smtpPassword: config('smtp_password') || process.env.SMTP_PASSWORD || '', 42 | kioskEnabled: config('kiosk_enabled') || (process.env.KIOSK_ENABLED === 'true') || false, 43 | kioskVoucherType: config('kiosk_voucher_type') || process.env.KIOSK_VOUCHER_TYPE || '480,1,,,', 44 | logLevel: config('log_level') || process.env.LOG_LEVEL || 'info', 45 | translationDefault: config('translation_default') || process.env.TRANSLATION_DEFAULT || 'en', 46 | translationDebug: config('translation_debug') || (process.env.TRANSLATION_DEBUG === 'true') || false, 47 | gitTag: process.env.GIT_TAG || 'master', 48 | gitBuild: fs.existsSync('/etc/unifi_voucher_site_build') ? fs.readFileSync('/etc/unifi_voucher_site_build', 'utf-8') : 'Development' 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unifi-voucher-site", 3 | "version": "0.0.0", 4 | "description": "NPM packages for unifi-voucher-site", 5 | "private": true, 6 | "scripts": { 7 | "start": "LOG_LEVEL=trace node server.js", 8 | "dev": "LOG_LEVEL=trace node --watch server.js", 9 | "tailwind": "tailwindcss -i ./css/style.css -o ./public/dist/style.css --watch", 10 | "build": "tailwindcss -i ./css/style.css -o ./public/dist/style.css --minify" 11 | }, 12 | "engines": { 13 | "node": ">=22.0.0" 14 | }, 15 | "author": "Glenn de Haan", 16 | "license": "MIT", 17 | "overrides": { 18 | "node-unifi@^2.5.1": { 19 | "axios": "1.8.3", 20 | "tough-cookie": "5.1.1" 21 | } 22 | }, 23 | "dependencies": { 24 | "cookie-parser": "^1.4.7", 25 | "ejs": "^3.1.10", 26 | "express": "^5.1.0", 27 | "express-locale": "^2.0.2", 28 | "express-openid-connect": "^2.18.1", 29 | "js-logger": "^1.6.1", 30 | "jsonwebtoken": "^9.0.2", 31 | "multer": "^1.4.5-lts.2", 32 | "node-thermal-printer": "^4.4.5", 33 | "node-unifi": "^2.5.1", 34 | "nodemailer": "^7.0.3", 35 | "pdfkit": "^0.17.1", 36 | "qrcode": "^1.5.4" 37 | }, 38 | "devDependencies": { 39 | "@tailwindcss/cli": "^4.1.7", 40 | "@tailwindcss/forms": "^0.5.10", 41 | "tailwindcss": "^4.1.7" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/icon/logo_192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/icon/logo_192x192.png -------------------------------------------------------------------------------- /public/images/icon/logo_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/icon/logo_256x256.png -------------------------------------------------------------------------------- /public/images/icon/logo_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/icon/logo_512x512.png -------------------------------------------------------------------------------- /public/images/kiosk_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/kiosk_bg.jpg -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/logo.png -------------------------------------------------------------------------------- /public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/logo_grayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/logo_grayscale.png -------------------------------------------------------------------------------- /public/images/logo_grayscale_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/logo_grayscale_dark.png -------------------------------------------------------------------------------- /public/images/screenshots/desktop_screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/desktop_screenshot_1.png -------------------------------------------------------------------------------- /public/images/screenshots/desktop_screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/desktop_screenshot_2.png -------------------------------------------------------------------------------- /public/images/screenshots/desktop_screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/desktop_screenshot_3.png -------------------------------------------------------------------------------- /public/images/screenshots/desktop_screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/desktop_screenshot_4.png -------------------------------------------------------------------------------- /public/images/screenshots/mobile_screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/mobile_screenshot_1.png -------------------------------------------------------------------------------- /public/images/screenshots/mobile_screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/mobile_screenshot_2.png -------------------------------------------------------------------------------- /public/images/screenshots/mobile_screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/mobile_screenshot_3.png -------------------------------------------------------------------------------- /public/images/screenshots/mobile_screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenndehaan/unifi-voucher-site/f20a582b1db34bc3bc18cb81ceb279671f86a841/public/images/screenshots/mobile_screenshot_4.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UniFi Voucher", 3 | "short_name": "Voucher", 4 | "description": "UniFi Voucher Site is a web-based platform for generating and managing UniFi network guest vouchers", 5 | "icons": [ 6 | { 7 | "src": "./images/icon/logo_192x192.png", 8 | "type": "image/png", 9 | "sizes": "192x192" 10 | }, 11 | { 12 | "src": "./images/icon/logo_256x256.png", 13 | "type": "image/png", 14 | "sizes": "256x256" 15 | }, 16 | { 17 | "src": "./images/icon/logo_512x512.png", 18 | "type": "image/png", 19 | "sizes": "512x512" 20 | } 21 | ], 22 | "screenshots": [ 23 | { 24 | "src": "./images/screenshots/mobile_screenshot_1.png", 25 | "sizes": "413x877", 26 | "type": "image/png", 27 | "form_factor": "narrow" 28 | }, 29 | { 30 | "src": "./images/screenshots/mobile_screenshot_2.png", 31 | "sizes": "413x877", 32 | "type": "image/png", 33 | "form_factor": "narrow" 34 | }, 35 | { 36 | "src": "./images/screenshots/mobile_screenshot_3.png", 37 | "sizes": "413x877", 38 | "type": "image/png", 39 | "form_factor": "narrow" 40 | }, 41 | { 42 | "src": "./images/screenshots/mobile_screenshot_4.png", 43 | "sizes": "413x877", 44 | "type": "image/png", 45 | "form_factor": "narrow" 46 | }, 47 | { 48 | "src": "./images/screenshots/desktop_screenshot_1.png", 49 | "sizes": "1280x720", 50 | "type": "image/png", 51 | "form_factor": "wide" 52 | }, 53 | { 54 | "src": "./images/screenshots/desktop_screenshot_2.png", 55 | "sizes": "1280x720", 56 | "type": "image/png", 57 | "form_factor": "wide" 58 | }, 59 | { 60 | "src": "./images/screenshots/desktop_screenshot_3.png", 61 | "sizes": "1280x720", 62 | "type": "image/png", 63 | "form_factor": "wide" 64 | }, 65 | { 66 | "src": "./images/screenshots/desktop_screenshot_4.png", 67 | "sizes": "1280x720", 68 | "type": "image/png", 69 | "form_factor": "wide" 70 | } 71 | ], 72 | "start_url": "/", 73 | "scope": "/", 74 | "display": "standalone", 75 | "categories": ["utilities"], 76 | "background_color": "#139CDA", 77 | "theme_color": "#139CDA", 78 | "related_applications": [], 79 | "iarc_rating_id": "" 80 | } 81 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is present to satisfy a requirement of the Tailwind CSS IntelliSense 3 | The rest of this file is intentionally empty. 4 | */ 5 | -------------------------------------------------------------------------------- /template/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Not Found | UniFi Voucher 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 | UniFi Voucher Site Logo 34 |

Page not found

35 |

Sorry, we couldn’t find the page you’re looking for.

36 |
37 | Go back home 38 |
39 |

40 | 41 | 42 | 43 | 44 | 45 |

46 |
47 |
48 | 49 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /template/500.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error | UniFi Voucher 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 | UniFi Voucher Site Logo 34 |

Error

35 |

Sorry, an unexpected error occurred.

36 |
37 | <%= error %>
38 | 
39 |
40 | Go back home 41 |
42 |

43 | 44 | 45 | 46 | 47 | 48 |

49 |
50 |
51 | 52 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /template/components/bulk-print.ejs: -------------------------------------------------------------------------------- 1 | 119 | -------------------------------------------------------------------------------- /template/components/email.ejs: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /template/components/print.ejs: -------------------------------------------------------------------------------- 1 | 55 | -------------------------------------------------------------------------------- /template/email/voucher.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= t('title') %> 7 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /template/kiosk.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kiosk | UniFi Voucher 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 | <% if(typeof voucherCode !== 'undefined') { %> 32 |
33 |
34 |
35 | <% } %> 36 | 37 |
38 | Kiosk Background 39 |
40 |
41 | 42 |
43 |
44 | <% if(typeof voucherCode === 'undefined') { %> 45 |
46 |
47 | 52 |
53 |
54 | <% } %> 55 | 56 | UniFi Voucher Site Logo 57 |

<%= t('title') %>

58 | 59 | <% if(error) { %> 60 |
61 |
62 |
63 | 66 |
67 |
68 |

<%= error_text %>

69 |
70 |
71 |
72 | <% } %> 73 |
74 | 75 |
76 | <% if(typeof voucherCode === 'undefined') { %> 77 |
78 |
79 | 88 |
89 | 102 |
103 | <% } else { %> 104 |
105 |
106 |

107 | <%= t('use') %>: 108 |

109 |
110 | <%= voucherCode %> 111 |
112 |
113 | <% if(unifiSsidPassword !== '') { %> 114 | <%= t('connect') %>: <%= unifiSsid %>,
115 | <%= t('password') %>: <%= unifiSsidPassword %> <%= t('or') %>,
116 | <% } else { %> 117 | <%= t('connect') %>: <%= unifiSsid %> <%= t('or') %>,
118 | <% } %> 119 | <%= t('scan') %>: 120 |
121 |
122 | Scan to Connect QR Code 123 |
124 |
125 | 126 | <% if(email_enabled) { %> 127 |
128 | <% if(typeof email === 'undefined') { %> 129 |
130 | 133 | 134 |
135 | 136 | 137 | 138 | 139 | 146 | 147 | 160 | <% } else { %> 161 |
162 | 163 | 164 | 165 | <%= t('sent') %> 166 |
167 | <% } %> 168 |
169 | <% } %> 170 |
171 | <% } %> 172 |
173 | 174 | <% if(typeof voucherCode !== 'undefined') { %> 175 | 184 | <% } %> 185 |
186 | 187 | 217 | <% if(typeof voucherCode !== 'undefined') { %> 218 | 235 | <% } %> 236 | 237 | 238 | -------------------------------------------------------------------------------- /template/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login | UniFi Voucher 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 | UniFi Voucher Site Logo 34 |

<%= app_header %>

35 | 36 | <% if(error) { %> 37 |
38 |
39 |
40 | 43 |
44 |
45 |

<%= error_text %>

46 |
47 |
48 |
49 | <% } %> 50 |
51 | 52 |
53 | <% if(internalAuth) { %> 54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 | 62 |
63 | 64 |
65 |
66 | <% } %> 67 | 68 | <% if(oidcAuth) { %> 69 |
70 | <% if(internalAuth) { %> 71 |
72 | 75 |
76 | Or continue with 77 |
78 |
79 | <% } %> 80 | 81 | 92 |
93 | <% } %> 94 | 95 |

96 | 97 | 98 | 99 | 100 | 101 |

102 |
103 |
104 | 105 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /template/partials/navigation.ejs: -------------------------------------------------------------------------------- 1 | 86 | 87 | 101 | -------------------------------------------------------------------------------- /template/partials/tag.ejs: -------------------------------------------------------------------------------- 1 | <% if(status.state === 'red') { %> 2 |
3 | <%= status.text %> 4 |
5 | <% } %> 6 | 7 | <% if(status.state === 'yellow') { %> 8 |
9 | <%= status.text %> 10 |
11 | <% } %> 12 | 13 | <% if(status.state === 'green') { %> 14 |
15 | <%= status.text %> 16 |
17 | <% } %> 18 | -------------------------------------------------------------------------------- /utils/array.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports the array util 3 | */ 4 | module.exports = { 5 | deprecated: [ 6 | 'SECURITY_CODE', 7 | 'DISABLE_AUTH', 8 | 'AUTH_OIDC_CLIENT_TYPE', 9 | 'AUTH_PASSWORD', 10 | 'AUTH_TOKEN', 11 | 'UI_BACK_BUTTON', 12 | 'PRINTER_TYPE', 13 | 'PRINTER_IP' 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /utils/bytes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts bytes to a human readable format 3 | * 4 | * @param bytes 5 | * @param type 6 | * @param perSecond 7 | * @param decimals 8 | * @return {string} 9 | */ 10 | module.exports = (bytes, type = 0, perSecond = false, decimals = 2) => { 11 | if (bytes === 0) return '0 Bytes'; 12 | 13 | const k = 1000; 14 | const dm = decimals < 0 ? 0 : decimals; 15 | const sizes = ['Bytes', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb'].toSpliced(0, type); 16 | 17 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 18 | 19 | const suffix = perSecond ? sizes[i] + 'ps' : sizes[i].toUpperCase(); 20 | 21 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + suffix; 22 | } 23 | -------------------------------------------------------------------------------- /utils/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import own modules 3 | */ 4 | const log = require('../modules/log'); 5 | const unifi = require('../modules/unifi'); 6 | const cache = require('../modules/cache'); 7 | 8 | /** 9 | * Exports all cache utils 10 | * 11 | * @type {{updateCache: (function(): Promise<*>)}} 12 | */ 13 | module.exports = { 14 | /** 15 | * Update the cache 16 | * 17 | * @return {Promise<*>} 18 | */ 19 | updateCache: () => { 20 | return new Promise(async (resolve) => { 21 | log.debug('[Cache] Requesting UniFi Vouchers...'); 22 | 23 | const vouchers = await unifi.list().catch(() => { 24 | log.error('[Cache] Error requesting vouchers!'); 25 | }); 26 | 27 | if(vouchers) { 28 | cache.vouchers = vouchers; 29 | cache.updated = new Date().getTime(); 30 | log.debug(`[Cache] Saved ${vouchers.length} voucher(s)`); 31 | } 32 | 33 | log.debug('[Cache] Requesting UniFi Guests...'); 34 | 35 | const guests = await unifi.guests().catch(() => { 36 | log.error('[Cache] Error requesting guests!'); 37 | }); 38 | 39 | if(guests) { 40 | cache.guests = guests; 41 | cache.updated = new Date().getTime(); 42 | log.debug(`[Cache] Saved ${guests.length} guest(s)`); 43 | } 44 | 45 | resolve(); 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /utils/languages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports all languages 3 | */ 4 | module.exports = { 5 | en: 'English', 6 | de: 'German', 7 | da: 'Danish', 8 | fr: 'French', 9 | nl: 'Dutch', 10 | es: 'Spanish', 11 | pl: 'Polish', 12 | ru: 'Russian' 13 | }; 14 | -------------------------------------------------------------------------------- /utils/logo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Output the ascii logo 3 | */ 4 | module.exports = () => { 5 | console.log(` __ __ _ _______ _ __ __ `); 6 | console.log(` / / / /___ (_) ____(_) | | / /___ __ _______/ /_ ___ _____`); 7 | console.log(` / / / / __ \\/ / /_ / / | | / / __ \\/ / / / ___/ __ \\/ _ \\/ ___/`); 8 | console.log(`/ /_/ / / / / / __/ / / | |/ / /_/ / /_/ / /__/ / / / __/ / `); 9 | console.log(`\\____/_/ /_/_/_/ /_/ |___/\\____/\\__,_/\\___/_/ /_/\\___/_/ `); 10 | console.log(''); 11 | console.log(' UniFi Voucher '); 12 | console.log(' By: Glenn de Haan '); 13 | console.log(' https://github.com/glenndehaan/unifi-voucher-site '); 14 | console.log(''); 15 | } 16 | -------------------------------------------------------------------------------- /utils/size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import own modules 3 | */ 4 | const variables = require('../modules/variables'); 5 | 6 | /** 7 | * Util function to calculate paper size based on voucher data 8 | */ 9 | module.exports = (voucher) => { 10 | let base = variables.unifiSsid !== '' ? variables.unifiSsidPassword !== '' ? 415 : 375 : 260; 11 | 12 | if(voucher.qos_usage_quota) { 13 | base += 10; 14 | } 15 | 16 | if(voucher.qos_rate_max_down) { 17 | base += 10; 18 | } 19 | 20 | if(voucher.qos_rate_max_up) { 21 | base += 10; 22 | } 23 | 24 | return base; 25 | } 26 | -------------------------------------------------------------------------------- /utils/status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import own modules 3 | */ 4 | const variables = require('../modules/variables'); 5 | 6 | /** 7 | * Util to return status of all application components and features 8 | * 9 | * @return {{}} 10 | */ 11 | module.exports = () => { 12 | return { 13 | app: { 14 | status: { 15 | text: variables.serviceWeb || variables.serviceApi ? 'Enabled' : 'Disabled', 16 | state: variables.serviceWeb || variables.serviceApi ? 'green' : 'red' 17 | }, 18 | details: variables.serviceWeb || variables.serviceApi ? 'Service has been configured.' : 'No services enabled.', 19 | info: 'https://github.com/glenndehaan/unifi-voucher-site#services', 20 | modules: { 21 | web: { 22 | status: { 23 | text: variables.serviceWeb ? 'Enabled' : 'Disabled', 24 | state: variables.serviceWeb ? 'green' : 'red' 25 | }, 26 | details: variables.serviceWeb || variables.serviceApi ? 'Service running on http://0.0.0.0:3000.' : 'Web service not enabled.', 27 | info: 'https://github.com/glenndehaan/unifi-voucher-site#web-service' 28 | }, 29 | api: { 30 | status: { 31 | text: variables.serviceApi ? 'Enabled' : 'Disabled', 32 | state: variables.serviceApi ? 'green' : 'red' 33 | }, 34 | details: variables.serviceWeb || variables.serviceApi ? 'Service running on http://0.0.0.0:3000/api.' : 'Api service not enabled.', 35 | info: 'https://github.com/glenndehaan/unifi-voucher-site#api-service' 36 | } 37 | } 38 | }, 39 | unifi: { 40 | status: { 41 | text: 'Enabled', 42 | state: 'green' 43 | }, 44 | details: `UniFi Voucher is connected with UniFi on: ${variables.unifiIp}:${variables.unifiPort}.`, 45 | info: 'https://github.com/glenndehaan/unifi-voucher-site#prerequisites', 46 | modules: {} 47 | }, 48 | printing: { 49 | status: { 50 | text: variables.printers !== '' ? 'Enabled' : 'Disabled', 51 | state: variables.printers !== '' ? 'green' : 'red' 52 | }, 53 | details: variables.printers !== '' ? `Printing service has been configured. Available printers: ${variables.printers.split(',').join(', ')}` : 'No printing service enabled.', 54 | info: 'https://github.com/glenndehaan/unifi-voucher-site#print-functionality', 55 | modules: {} 56 | }, 57 | email: { 58 | status: { 59 | text: (variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '') ? 'Enabled' : 'Disabled', 60 | state: (variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '') ? 'green' : 'red' 61 | }, 62 | details: (variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '') ? `Email service sending to ${variables.smtpHost}:${variables.smtpPort}.` : 'Email service not enabled.', 63 | info: 'https://github.com/glenndehaan/unifi-voucher-site#email-functionality', 64 | modules: {} 65 | }, 66 | kiosk: { 67 | status: { 68 | text: variables.kioskEnabled ? 'Enabled' : 'Disabled', 69 | state: variables.kioskEnabled ? 'green' : 'red' 70 | }, 71 | details: variables.kioskEnabled ? `Kiosk service enabled on http://0.0.0.0:3000/kiosk.` : 'Kiosk service not enabled.', 72 | info: 'https://github.com/glenndehaan/unifi-voucher-site#kiosk-functionality', 73 | modules: {} 74 | }, 75 | authentication: { 76 | status: { 77 | text: !variables.authDisabled ? 'Enabled' : 'Disabled', 78 | state: !variables.authDisabled ? 'green' : 'red' 79 | }, 80 | details: !variables.authDisabled ? 'Authentication service has been configured.' : 'Authentication has been disabled.', 81 | info: 'https://github.com/glenndehaan/unifi-voucher-site#authentication', 82 | modules: { 83 | internal: { 84 | status: { 85 | text: (!variables.authDisabled && variables.authInternalEnabled) ? 'Enabled' : 'Disabled', 86 | state: (!variables.authDisabled && variables.authInternalEnabled) ? 'green' : 'red' 87 | }, 88 | details: (!variables.authDisabled && variables.authInternalEnabled) ? 'Internal Authentication enabled.' : 'Internal Authentication not enabled.', 89 | info: 'https://github.com/glenndehaan/unifi-voucher-site#1-internal-authentication-default' 90 | }, 91 | oidc: { 92 | status: { 93 | text: (!variables.authDisabled && variables.authOidcEnabled) ? 'Enabled' : 'Disabled', 94 | state: (!variables.authDisabled && variables.authOidcEnabled) ? 'green' : 'red' 95 | }, 96 | details: (!variables.authDisabled && variables.authOidcEnabled) ? `OIDC Authentication via ${variables.authOidcIssuerBaseUrl}.` : 'OIDC Authentication not enabled.', 97 | info: 'https://github.com/glenndehaan/unifi-voucher-site#2-openid-connect-oidc-authentication' 98 | } 99 | } 100 | } 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /utils/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert time minutes 3 | * 4 | * @param minutes 5 | * @returns {string} 6 | */ 7 | module.exports = (minutes) => { 8 | if (minutes < 60) { 9 | return `${minutes} minute(s)`; 10 | } 11 | 12 | const hours = minutes / 60; 13 | 14 | if (hours < 24) { 15 | return `${hours % 1 === 0 ? hours : hours.toFixed(2)} ${(hours > 1) ? 'hours' : 'hour'}`; 16 | } 17 | 18 | const days = hours / 24; 19 | return `${days} ${(days > 1) ? 'days' : 'day'}`; 20 | } 21 | -------------------------------------------------------------------------------- /utils/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an array or object of voucher type(s) 3 | * 4 | * @param string 5 | * @param single 6 | * @returns {*} 7 | */ 8 | module.exports = (string, single = false) => { 9 | if(single) { 10 | const match = string.match(/^(?\d+)?,(?\d+)?,(?\d+)?,(?\d+)?,(?\d+)?/); 11 | return match.groups.expiration ? {...match.groups, raw: string} : undefined; 12 | } 13 | 14 | const types = string.split(';'); 15 | 16 | return types.filter(n => n).map((type) => { 17 | const match = type.match(/^(?\d+)?,(?\d+)?,(?\d+)?,(?\d+)?,(?\d+)?/); 18 | return match.groups.expiration ? {...match.groups, raw: type} : undefined; 19 | }).filter(n => n); 20 | } 21 | --------------------------------------------------------------------------------