initial commit
All checks were successful
Build and Publish / Build Yale Access Backend (push) Successful in 28s
Build and Publish / Build Yale Access Frontend (push) Successful in 47s
Build and Publish / Push Yale Access Backend Docker Image (push) Successful in 9s
Build and Publish / Push Yale Access Frontend Docker Image (push) Successful in 10s
All checks were successful
Build and Publish / Build Yale Access Backend (push) Successful in 28s
Build and Publish / Build Yale Access Frontend (push) Successful in 47s
Build and Publish / Push Yale Access Backend Docker Image (push) Successful in 9s
Build and Publish / Push Yale Access Frontend Docker Image (push) Successful in 10s
This commit is contained in:
commit
f577617b4d
133
.github/workflows/ci.yml
vendored
Normal file
133
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
name: Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- feature/*
|
||||
- fix/*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-backend:
|
||||
name: Build Yale Access Backend
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/backend
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
publish-backend:
|
||||
name: Push Yale Access Backend Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-backend
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Docker Metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: liamsgit.dev/LiamPietralla/yale-user-access-backend
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
org.opencontainers.image.title=Yale Access Backend
|
||||
org.opencontainers.image.description=Backend for Yale Access
|
||||
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: liamsgit.dev
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
context: ./packages/backend
|
||||
file: ./packages/backend/Dockerfile
|
||||
|
||||
build-frontend:
|
||||
name: Build Yale Access Frontend
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Setup Yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build
|
||||
run: yarn run build
|
||||
|
||||
publish-frontend:
|
||||
name: Push Yale Access Frontend Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-frontend
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Docker Metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: liamsgit.dev/LiamPietralla/yale-user-access-frontend
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
org.opencontainers.image.title=Yale Access Frontend
|
||||
org.opencontainers.image.description=Frontend for Yale Access
|
||||
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: liamsgit.dev
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
context: ./packages/frontend
|
||||
file: ./packages/frontend/Dockerfile
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.vs
|
83
README.md
Normal file
83
README.md
Normal file
@ -0,0 +1,83 @@
|
||||
# yale-access
|
||||
|
||||
Yale Access is a solution to allow passcodes to be added and removed to a Yale Smart Lock.
|
||||
|
||||
The solution is designed to be self-hosted, as it will need access to a ZWave.JS server to communicate with the lock.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A few pre-requisites are required for this application:
|
||||
|
||||
* A Yale Smart Lock (I have an Assure Lock SL)
|
||||
* A ZWave.JS server
|
||||
|
||||
## Installation
|
||||
|
||||
Yale Access is best run as a Docker container, as this reduces almost all hosting compexity. The application will need to be hosted on the internal network on your home (though the application can be exposed to the internet if you wish).
|
||||
|
||||
### Preparing the lock
|
||||
|
||||
The lock will need to be paired with a ZWave.JS server, I suggest using ![ZWave.JS UI](https://github.com/zwave-js/zwave-js-ui). Once the lock is paired you will need to set a value for all the user codes you wish to use (this prevents null values being returned from the lock). To do this navigate to the lock in the ZWave.JS UI, and click the 'User Code v1' option. For each user code you are intending to use set the status to 'Available'.
|
||||
|
||||
### Docker
|
||||
|
||||
A sample docker compose file is provided in the repository. This can be used to get up and running quickly.
|
||||
|
||||
The appropriate environment variables will need to be set in the docker compose file. These are:
|
||||
|
||||
##### Frontend
|
||||
|
||||
* NUXT_PUBLIC_API_BASE_URL - The URL of the backend API
|
||||
|
||||
##### Backend
|
||||
|
||||
* LogLocation - The location to store logs, this path should also be mounted as a local volume, to ensure logs are not lost when the container is restarted.
|
||||
* CorsAllowedOrigins - A comma separated list of allowed origins for CORS requests. This should be set to the URL of the frontend.
|
||||
* Devices__YaleLockNodeId - The Node ID of the Yale Lock as found in the ZWave.JS interface.
|
||||
* Codes__Home - The code ID to use as the 'home' code. This will have extra UI functionality, to prevent it from being updated or deleted accidentally.
|
||||
* Codes__GuestCodeRangeStart - The start of the range of codes to use for guest codes. This should be set to the first code that is available for use.
|
||||
* Codes__GuestCodeRangeCount - The number of codes to use for guest codes. This should be set to the number of codes that are available for use.
|
||||
* ZWave_Url - The URL of the ZWave.JS server.
|
||||
* ZWave_SchemeVersion - The scheme version of the ZWave.JS server. This should be set to the scheme version of the ZWave.JS server.
|
||||
* Authentication_Password - The password to use for authentication. This should be set to a strong password.
|
||||
* Twilio__AccountSid - The Account SID for the Twilio account to use for sending SMS messages.
|
||||
* Twilio__AuthToken - The Auth Token for the Twilio account to use for sending SMS messages.
|
||||
* Twilio__FromNumber - The number to send SMS messages from.
|
||||
|
||||
Once the environment variables have been set, the application can be started by running `docker-compose up -d` from the root of the compose file. To allow the application to be accessed from the internet, you will need to expose the application to the internet, the recommended setup would be a Nginx reverse proxy.
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Login
|
||||
|
||||
<img src="./screenshots/login.png" alt="Login" width="300"/>
|
||||
|
||||
### Home Screen
|
||||
|
||||
<img src="./screenshots/home.png" alt="Home" width="300"/>
|
||||
|
||||
### Edit Code
|
||||
|
||||
<img src="./screenshots/edit.png" alt="Edit Code" width="300"/>
|
||||
|
||||
### Delete Code
|
||||
|
||||
<img src="./screenshots/delete.png" alt="Delete Code" width="300"/>
|
||||
|
||||
## Development
|
||||
|
||||
### Frontend
|
||||
|
||||
The frontend is built using Nuxt. To get started ensure you have node install (latest LTS is recommended), and run `yarn install` to install all dependencies.
|
||||
|
||||
To start the development server, run `yarn dev`.
|
||||
|
||||
### Backend
|
||||
|
||||
The backend is built using .NET 7. To get started ensure you have the .NET 7 SDK installed. Open the solution in Visual Studio and ensure that the appropriate user secrets are set. A `secrets.template.json` file is provided in the repository. This should be copied to `secrets.json` and populated with the appropriate values.
|
||||
|
||||
To start the backend, run `dotnet run` from the `YaleAccess.Api` directory (or use the Visual Studio debugger).
|
||||
|
||||
Note: you will need the ZWave.JS server running and configured to use the same scheme version as the backend for development unsless you are using the mock mode.
|
||||
|
||||
To enable the mock mode for development, set the `UseMockDevelopmentMode` flag to true in the `appsettings.Development.json` file. This will repalce teh ZWave.JS server with a mock implementation, which will allow you to test the application without a ZWave.JS server. This is handy for doing UI changes without having to have the lock available.
|
3
development/.gitignore
vendored
Normal file
3
development/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
yale.db
|
||||
yale.db-shm
|
||||
yale.db-wal
|
25
packages/backend/.dockerignore
Normal file
25
packages/backend/.dockerignore
Normal file
@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
424
packages/backend/.gitignore
vendored
Normal file
424
packages/backend/.gitignore
vendored
Normal file
@ -0,0 +1,424 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# and the packages/backend and packages/frontend folders, since they contain our source code
|
||||
!**/[Pp]ackages/backend/
|
||||
!**/[Pp]ackages/frontend/
|
||||
!**/[Pp]ackages/frontend-old/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
# Frontend
|
||||
.DS_Store
|
||||
node_modules/
|
||||
/dist/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sw*
|
76
packages/backend/Controllers/AuthenticationController.cs
Normal file
76
packages/backend/Controllers/AuthenticationController.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
using System.Security.Claims;
|
||||
using YaleAccess.Models;
|
||||
|
||||
namespace YaleAccess.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[EnableCors]
|
||||
[Authorize]
|
||||
public class AuthenticationController : ControllerBase
|
||||
{
|
||||
private readonly Models.Options.AuthenticationOptions _authenticationOptions;
|
||||
|
||||
public AuthenticationController(IOptions<Models.Options.AuthenticationOptions> authenticationOptions)
|
||||
{
|
||||
_authenticationOptions = authenticationOptions.Value;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromBody] string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if the password is correct
|
||||
if (password != _authenticationOptions.Password)
|
||||
{
|
||||
return Unauthorized(new ApiResponse("Incorrect password."));
|
||||
}
|
||||
|
||||
// Log the user in
|
||||
List<Claim> claims = new()
|
||||
{
|
||||
new Claim(ClaimTypes.Name, "YaleAccess")
|
||||
};
|
||||
ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
ClaimsPrincipal claimsPrincipal = new(claimsIdentity);
|
||||
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
|
||||
|
||||
// Return the response
|
||||
return Ok(new ApiResponse(true));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occurred logging in.");
|
||||
return BadRequest(new ApiResponse("An error occurred logging in."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Sign the user out
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
return Ok(new ApiResponse(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occured logging out.");
|
||||
return BadRequest(new ApiResponse("An error occured logging out."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
packages/backend/Controllers/HealthController.cs
Normal file
22
packages/backend/Controllers/HealthController.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Serilog;
|
||||
|
||||
namespace YaleAccess.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[EnableCors]
|
||||
[Authorize]
|
||||
public class HealthController : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Health()
|
||||
{
|
||||
Log.Logger.Information("Hit the health endpoint.");
|
||||
return Ok("Service is healthy");
|
||||
}
|
||||
}
|
||||
}
|
84
packages/backend/Controllers/PeopleController.cs
Normal file
84
packages/backend/Controllers/PeopleController.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using YaleAccess.Data;
|
||||
using YaleAccess.Models;
|
||||
|
||||
namespace YaleAccess.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[EnableCors]
|
||||
[Authorize]
|
||||
public class PeopleController : ControllerBase
|
||||
{
|
||||
private readonly YaleContext _context;
|
||||
|
||||
public PeopleController(YaleContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetPeople()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Return all people
|
||||
List<Person> people = await _context.People.ToListAsync();
|
||||
return Ok(new ApiResponse(people));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occured retriving the people.");
|
||||
return BadRequest(new ApiResponse("An error occured retriving the people."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreatePerson([FromBody] Person person)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a new person
|
||||
Person newPerson = new() { Name = person.Name, PhoneNumber = person.PhoneNumber };
|
||||
|
||||
// Add the person
|
||||
await _context.AddAsync(newPerson);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Return the newly created person
|
||||
return Ok(new ApiResponse(newPerson));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occured creating the person.");
|
||||
return BadRequest(new ApiResponse("An error occured creating the person."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeletePerson([FromRoute] int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure the person exists
|
||||
Person person = await _context.People.FindAsync(id) ?? throw new Exception("Person not found.");
|
||||
|
||||
// Remove the person
|
||||
_context.Remove(person);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Return the newly removed person
|
||||
return Ok(new ApiResponse(person));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occured deletiong the person.");
|
||||
return BadRequest(new ApiResponse("An error occured deletiong the person."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
148
packages/backend/Controllers/YaleController.cs
Normal file
148
packages/backend/Controllers/YaleController.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
using YaleAccess.Models;
|
||||
using YaleAccess.Models.Options;
|
||||
using YaleAccess.Services;
|
||||
using YaleAccess.Services.Interfaces;
|
||||
|
||||
namespace YaleAccess.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[EnableCors]
|
||||
[Authorize]
|
||||
public class YaleController : ControllerBase
|
||||
{
|
||||
private readonly IYaleAccessor _yaleAccessor;
|
||||
private readonly CodesOptions _codeOptions;
|
||||
private readonly SMSService _smsService;
|
||||
|
||||
public YaleController(IYaleAccessor yaleAccessor, IOptions<CodesOptions> codeOptions, SMSService smsService)
|
||||
{
|
||||
_yaleAccessor = yaleAccessor;
|
||||
_codeOptions = codeOptions.Value;
|
||||
_smsService = smsService;
|
||||
}
|
||||
|
||||
[HttpGet("codes")]
|
||||
public async Task<IActionResult> GetUserCodes()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the home code first
|
||||
YaleUserCode homeCode = await _yaleAccessor.GetCodeInformationAsync(_codeOptions.Home);
|
||||
homeCode.IsHome = true;
|
||||
|
||||
// Get the guest codes
|
||||
List<YaleUserCode> guestCodes = new();
|
||||
foreach (int code in Enumerable.Range(_codeOptions.GuestCodeRangeStart, _codeOptions.GuestCodeRangeCount))
|
||||
{
|
||||
guestCodes.Add(await _yaleAccessor.GetCodeInformationAsync(code));
|
||||
}
|
||||
|
||||
// Add the home code to the list
|
||||
guestCodes.Add(homeCode);
|
||||
|
||||
// Return the codes
|
||||
return Ok(new ApiResponse(guestCodes));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occurred retriving the codes.");
|
||||
return BadRequest(new ApiResponse("An error occurred retriving the codes."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("code/{id}")]
|
||||
public async Task<IActionResult> SetUserCode([FromRoute] int id, [FromBody] string newCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
// First validate the user code
|
||||
string validCode = YaleAccessor.ValidateCode(newCode);
|
||||
if (validCode != string.Empty)
|
||||
{
|
||||
return BadRequest(new ApiResponse(validCode));
|
||||
}
|
||||
|
||||
// Set the new code
|
||||
bool result = await _yaleAccessor.SetUserCode(id, newCode);
|
||||
|
||||
// Return the result
|
||||
if (result)
|
||||
{
|
||||
Log.Logger.Information("Updated code for user {id} to {code}", id, newCode);
|
||||
return Ok(new ApiResponse(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Logger.Information("Failed to update code for user {id} to {code}", id, newCode);
|
||||
return BadRequest(new ApiResponse("An error occurred setting the code."));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occurred setting the code.");
|
||||
return BadRequest(new ApiResponse("An error occurred setting the code."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("code/{id}/status")]
|
||||
public async Task<IActionResult> SetUserCodeStatusAsAvailable([FromRoute] int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
// First validate the user code
|
||||
string validCode = YaleAccessor.ValidateClearCode(id, _codeOptions.Home);
|
||||
if (validCode != string.Empty)
|
||||
{
|
||||
return BadRequest(new ApiResponse(validCode));
|
||||
}
|
||||
|
||||
// Set the available status
|
||||
bool result = await _yaleAccessor.SetCodeAsAvailable(id);
|
||||
|
||||
// Return the result
|
||||
if (result)
|
||||
{
|
||||
Log.Logger.Information("Updated code status for user {id} to available", id);
|
||||
return Ok(new ApiResponse(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Logger.Information("Failed to update code status for user {id} to available", id);
|
||||
return BadRequest(new ApiResponse("An error occurred setting the code status."));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occurred setting the code status.");
|
||||
return BadRequest(new ApiResponse("An error occurred setting the code status."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("code/{id}/send")]
|
||||
public async Task<IActionResult> SendUserCode([FromRoute] int id, [FromBody] string phoneNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the user code
|
||||
YaleUserCode userCode = await _yaleAccessor.GetCodeInformationAsync(id);
|
||||
|
||||
// Send the code via SMS to the phone number
|
||||
await _smsService.SendCodeViaSMSAsync(userCode.Code, phoneNumber);
|
||||
|
||||
// Return success
|
||||
return Ok(new ApiResponse(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "An error occurred sending the code.");
|
||||
return BadRequest(new ApiResponse("An error occurred sending the code."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
packages/backend/Data/Person.cs
Normal file
9
packages/backend/Data/Person.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace YaleAccess.Data
|
||||
{
|
||||
public class Person
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string PhoneNumber { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
11
packages/backend/Data/YaleContext.cs
Normal file
11
packages/backend/Data/YaleContext.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace YaleAccess.Data
|
||||
{
|
||||
public class YaleContext : DbContext
|
||||
{
|
||||
public YaleContext(DbContextOptions<YaleContext> options) : base(options) { }
|
||||
|
||||
public DbSet<Person> People { get; set; }
|
||||
}
|
||||
}
|
21
packages/backend/Dockerfile
Normal file
21
packages/backend/Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["YaleAccess.csproj", "."]
|
||||
RUN dotnet restore "./YaleAccess.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/."
|
||||
RUN dotnet build "YaleAccess.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "YaleAccess.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
RUN mkdir /yale-data
|
||||
ENV ConnectionStrings__YaleContext="Data Source=/yale-data/yale-access.db"
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "YaleAccess.dll"]
|
43
packages/backend/Migrations/20250109010459_AddedPeople.Designer.cs
generated
Normal file
43
packages/backend/Migrations/20250109010459_AddedPeople.Designer.cs
generated
Normal file
@ -0,0 +1,43 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using YaleAccess.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YaleAccess.Migrations
|
||||
{
|
||||
[DbContext(typeof(YaleContext))]
|
||||
[Migration("20250109010459_AddedPeople")]
|
||||
partial class AddedPeople
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.20");
|
||||
|
||||
modelBuilder.Entity("YaleAccess.Data.Person", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
35
packages/backend/Migrations/20250109010459_AddedPeople.cs
Normal file
35
packages/backend/Migrations/20250109010459_AddedPeople.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YaleAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedPeople : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "People",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
PhoneNumber = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_People", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "People");
|
||||
}
|
||||
}
|
||||
}
|
40
packages/backend/Migrations/YaleContextModelSnapshot.cs
Normal file
40
packages/backend/Migrations/YaleContextModelSnapshot.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using YaleAccess.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YaleAccess.Migrations
|
||||
{
|
||||
[DbContext(typeof(YaleContext))]
|
||||
partial class YaleContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.20");
|
||||
|
||||
modelBuilder.Entity("YaleAccess.Data.Person", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
29
packages/backend/Models/ApiResponse.cs
Normal file
29
packages/backend/Models/ApiResponse.cs
Normal file
@ -0,0 +1,29 @@
|
||||
namespace YaleAccess.Models
|
||||
{
|
||||
public class ApiResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public object? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If passing in data only then the resonse is successful
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
public ApiResponse(object data)
|
||||
{
|
||||
Success = true;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If passing in an error then the response is not successful
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
public ApiResponse(string error)
|
||||
{
|
||||
Success = false;
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
}
|
9
packages/backend/Models/Options/AuthenticationOptions.cs
Normal file
9
packages/backend/Models/Options/AuthenticationOptions.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace YaleAccess.Models.Options
|
||||
{
|
||||
public class AuthenticationOptions
|
||||
{
|
||||
public const string Authentication = "Authentication";
|
||||
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
11
packages/backend/Models/Options/CodesOptions.cs
Normal file
11
packages/backend/Models/Options/CodesOptions.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace YaleAccess.Models.Options
|
||||
{
|
||||
public class CodesOptions
|
||||
{
|
||||
public const string Codes = "Codes";
|
||||
|
||||
public int Home { get; set; }
|
||||
public int GuestCodeRangeStart { get; set; }
|
||||
public int GuestCodeRangeCount { get; set; }
|
||||
}
|
||||
}
|
9
packages/backend/Models/Options/DevicesOptions.cs
Normal file
9
packages/backend/Models/Options/DevicesOptions.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace YaleAccess.Models.Options
|
||||
{
|
||||
public class DevicesOptions
|
||||
{
|
||||
public const string Devices = "Devices";
|
||||
|
||||
public int YaleLockNodeId { get; set; }
|
||||
}
|
||||
}
|
12
packages/backend/Models/Options/TwiloOptions.cs
Normal file
12
packages/backend/Models/Options/TwiloOptions.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace YaleAccess.Models.Options
|
||||
{
|
||||
public class TwiloOptions
|
||||
{
|
||||
public const string Twilio = "Twilio";
|
||||
|
||||
public string AccountSid { get; set; } = string.Empty;
|
||||
public string AuthToken { get; set; } = string.Empty;
|
||||
public string FromNumber { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
10
packages/backend/Models/Options/ZWaveOptions.cs
Normal file
10
packages/backend/Models/Options/ZWaveOptions.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace YaleAccess.Models.Options
|
||||
{
|
||||
public class ZWaveOptions
|
||||
{
|
||||
public const string ZWave = "ZWave";
|
||||
|
||||
public string Url { get; set; } = null!;
|
||||
public int SchemaVersion { get; set; }
|
||||
}
|
||||
}
|
45
packages/backend/Models/YaleModels.cs
Normal file
45
packages/backend/Models/YaleModels.cs
Normal file
@ -0,0 +1,45 @@
|
||||
namespace YaleAccess.Models
|
||||
{
|
||||
public class YaleUserCode
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user code. When making requets to ZWave.JS API, this is the value to use.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The code / pin number for the user code. This may be null if the user code is not set or is not in use.
|
||||
/// Even though its a string, it is a number and appropriate validation is required.
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The current status of the user code.
|
||||
/// </summary>
|
||||
public UserCodeStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is true is this is the 'home' or normal user code for daily use.
|
||||
/// </summary>
|
||||
public bool IsHome { get; set; }
|
||||
}
|
||||
|
||||
public enum UserCodeStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// This means the code is not being used, and can be assigned
|
||||
/// </summary>
|
||||
AVAILABLE = 0,
|
||||
|
||||
/// <summary>
|
||||
/// This means that the code is in used, and cannot be assigned
|
||||
/// </summary>
|
||||
ENABLED = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Not sure what this means yet, perhaps it is related to schedules?
|
||||
/// </summary>
|
||||
DISABLED = 2
|
||||
}
|
||||
|
||||
}
|
154
packages/backend/Program.cs
Normal file
154
packages/backend/Program.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using YaleAccess.Data;
|
||||
using YaleAccess.Models.Options;
|
||||
using YaleAccess.Services;
|
||||
using YaleAccess.Services.Interfaces;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Create the bootstraper logger
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// Add the database context
|
||||
builder.Services.AddDbContext<YaleContext>(options =>
|
||||
{
|
||||
options.UseSqlite(builder.Configuration.GetConnectionString(nameof(YaleContext)));
|
||||
});
|
||||
|
||||
// Configure the options
|
||||
builder.Services.Configure<AuthenticationOptions>(builder.Configuration.GetSection(AuthenticationOptions.Authentication));
|
||||
builder.Services.Configure<CodesOptions>(builder.Configuration.GetSection(CodesOptions.Codes));
|
||||
builder.Services.Configure<DevicesOptions>(builder.Configuration.GetSection(DevicesOptions.Devices));
|
||||
builder.Services.Configure<ZWaveOptions>(builder.Configuration.GetSection(ZWaveOptions.ZWave));
|
||||
builder.Services.Configure<TwiloOptions>(builder.Configuration.GetSection(TwiloOptions.Twilio));
|
||||
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// Get a copy of the configuration
|
||||
IConfiguration configuration = builder.Configuration;
|
||||
string logLocation = configuration["LogLocation"] ?? "Log.txt";
|
||||
|
||||
// Setup the application logger
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Error)
|
||||
.WriteTo.File(logLocation, rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
// Configure the DI services
|
||||
builder.Services.AddScoped<SMSService>();
|
||||
|
||||
// If the environment is development and the mock option is set to true, use the mock service
|
||||
if (builder.Environment.IsDevelopment() && configuration.GetValue<bool>("UseMockDevelopmentMode"))
|
||||
{
|
||||
builder.Services.AddSingleton<MockYaleData>();
|
||||
builder.Services.AddScoped<IYaleAccessor, MockYaleAccessor>();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddScoped<IYaleAccessor, YaleAccessor>();
|
||||
}
|
||||
|
||||
|
||||
// Setup CORS
|
||||
string[] corsAllowedOrigins = (configuration["CorsAllowedOrigins"] ?? "http://localhost:3000").Split(",") ;
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.WithOrigins(corsAllowedOrigins)
|
||||
.AllowCredentials()
|
||||
.AllowAnyMethod()
|
||||
.WithHeaders("Content-Type", "Authorization");
|
||||
});
|
||||
});
|
||||
|
||||
// Setup cookie authentication scheme
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
// Set HTTP only to true to prevent XSS attacks
|
||||
options.Cookie.HttpOnly = true;
|
||||
|
||||
// Set secure policy to always to prevent sending cookies over HTTP for production use, for development set to None
|
||||
if (builder.Environment.IsDevelopment())
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.None;
|
||||
else
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
|
||||
// Ensure that the API will return a 401 Unauthorized instead of redirecting to the login page
|
||||
options.AccessDeniedPath = string.Empty;
|
||||
options.LoginPath = string.Empty;
|
||||
options.Events.OnRedirectToLogin = context =>
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
});
|
||||
|
||||
// Setup logging flow
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Create the database if it doesn't exist
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
var context = services.GetRequiredService<YaleContext>();
|
||||
context.Database.Migrate();
|
||||
}
|
||||
|
||||
// Configure the forwarded headers middleware
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
});
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore host aborted exceptions caused by build checks
|
||||
if (ex is not HostAbortedException)
|
||||
{
|
||||
Log.Fatal(ex, "Host terminated unexpectedly");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
51
packages/backend/Properties/launchSettings.json
Normal file
51
packages/backend/Properties/launchSettings.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5051"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7069;http://localhost:5051"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_URLS": "https://+:443;http://+:80"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
},
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:62005",
|
||||
"sslPort": 44335
|
||||
}
|
||||
}
|
||||
}
|
11
packages/backend/Services/Interfaces/IYaleAccessor.cs
Normal file
11
packages/backend/Services/Interfaces/IYaleAccessor.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using YaleAccess.Models;
|
||||
|
||||
namespace YaleAccess.Services.Interfaces
|
||||
{
|
||||
public interface IYaleAccessor
|
||||
{
|
||||
public Task<YaleUserCode> GetCodeInformationAsync(int userCodeId);
|
||||
public Task<bool> SetUserCode(int userCodeId, string code);
|
||||
public Task<bool> SetCodeAsAvailable(int userCode);
|
||||
}
|
||||
}
|
30
packages/backend/Services/MockYaleAccessor.cs
Normal file
30
packages/backend/Services/MockYaleAccessor.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using YaleAccess.Models;
|
||||
using YaleAccess.Services.Interfaces;
|
||||
|
||||
namespace YaleAccess.Services
|
||||
{
|
||||
public class MockYaleAccessor : IYaleAccessor
|
||||
{
|
||||
private readonly MockYaleData _data;
|
||||
|
||||
public MockYaleAccessor(MockYaleData data)
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public Task<YaleUserCode> GetCodeInformationAsync(int userCodeId)
|
||||
{
|
||||
return Task.FromResult(_data.GetUserCode(userCodeId));
|
||||
}
|
||||
|
||||
public Task<bool> SetCodeAsAvailable(int userCode)
|
||||
{
|
||||
return Task.FromResult(_data.SetUserCodeAsAvailable(userCode));
|
||||
}
|
||||
|
||||
public Task<bool> SetUserCode(int userCodeId, string code)
|
||||
{
|
||||
return Task.FromResult(_data.SetUserCode(userCodeId, code));
|
||||
}
|
||||
}
|
||||
}
|
78
packages/backend/Services/MockYaleData.cs
Normal file
78
packages/backend/Services/MockYaleData.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using YaleAccess.Models;
|
||||
|
||||
namespace YaleAccess.Services
|
||||
{
|
||||
public class MockYaleData
|
||||
{
|
||||
public List<YaleUserCode> UserCodes { get; set; } = new();
|
||||
|
||||
public MockYaleData()
|
||||
{
|
||||
// Create the home code
|
||||
YaleUserCode homeCode = new()
|
||||
{
|
||||
Id = 1,
|
||||
Code = "1234",
|
||||
IsHome = true,
|
||||
Status = UserCodeStatus.ENABLED
|
||||
};
|
||||
|
||||
// Create 5 guest codes
|
||||
List<YaleUserCode> guestCodes = new();
|
||||
foreach (int code in Enumerable.Range(2, 5))
|
||||
{
|
||||
guestCodes.Add(new YaleUserCode()
|
||||
{
|
||||
Id = code,
|
||||
Code = "",
|
||||
IsHome = false,
|
||||
Status = UserCodeStatus.AVAILABLE
|
||||
});
|
||||
}
|
||||
|
||||
// Add the home code to the list
|
||||
guestCodes.Add(homeCode);
|
||||
|
||||
// Set the user codes
|
||||
UserCodes = guestCodes;
|
||||
}
|
||||
|
||||
public YaleUserCode GetUserCode(int id)
|
||||
{
|
||||
return UserCodes.First(x => x.Id == id);
|
||||
}
|
||||
|
||||
public bool SetUserCode(int id, string code)
|
||||
{
|
||||
// Get the user code
|
||||
YaleUserCode userCode = GetUserCode(id);
|
||||
|
||||
// Set the code
|
||||
userCode.Code = code;
|
||||
|
||||
// Update code status
|
||||
userCode.Status = UserCodeStatus.ENABLED;
|
||||
|
||||
// Update the user code in the list
|
||||
UserCodes[UserCodes.IndexOf(userCode)] = userCode;
|
||||
|
||||
// Return true to indicate success
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetUserCodeAsAvailable(int id)
|
||||
{
|
||||
// Get the user code
|
||||
YaleUserCode userCode = GetUserCode(id);
|
||||
|
||||
// Set the code
|
||||
userCode.Status = UserCodeStatus.AVAILABLE;
|
||||
|
||||
// Update the user code in the list
|
||||
UserCodes[UserCodes.IndexOf(userCode)] = userCode;
|
||||
|
||||
// Return true to indicate success
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
35
packages/backend/Services/SMSService.cs
Normal file
35
packages/backend/Services/SMSService.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
using Twilio;
|
||||
using Twilio.Rest.Api.V2010.Account;
|
||||
using YaleAccess.Models.Options;
|
||||
|
||||
namespace YaleAccess.Services
|
||||
{
|
||||
public class SMSService
|
||||
{
|
||||
private readonly TwiloOptions _twiloOptions;
|
||||
|
||||
public SMSService(IOptions<TwiloOptions> twiloOptions)
|
||||
{
|
||||
_twiloOptions = twiloOptions.Value;
|
||||
}
|
||||
|
||||
public async Task SendCodeViaSMSAsync(string code, string phoneNumber)
|
||||
{
|
||||
// Create a Twilio client
|
||||
TwilioClient.Init(_twiloOptions.AccountSid, _twiloOptions.AuthToken);
|
||||
|
||||
// Send the message
|
||||
var message = await MessageResource.CreateAsync(
|
||||
body: $"{_twiloOptions.Message} {code}",
|
||||
from: new Twilio.Types.PhoneNumber(_twiloOptions.FromNumber),
|
||||
to: new Twilio.Types.PhoneNumber(phoneNumber)
|
||||
);
|
||||
|
||||
// Log the message
|
||||
Log.Logger.Information("SMS sent to {PhoneNumber} with message SID {MessageSid}.", phoneNumber, message.Sid);
|
||||
Log.Logger.Debug("SMS message: {MessageBody}", message.Body);
|
||||
}
|
||||
}
|
||||
}
|
195
packages/backend/Services/YaleAccessor.cs
Normal file
195
packages/backend/Services/YaleAccessor.cs
Normal file
@ -0,0 +1,195 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using YaleAccess.Models;
|
||||
using YaleAccess.Models.Options;
|
||||
using YaleAccess.Services.Interfaces;
|
||||
using ZWaveJS.NET;
|
||||
using ZWaveOptions = YaleAccess.Models.Options.ZWaveOptions;
|
||||
|
||||
namespace YaleAccess.Services
|
||||
{
|
||||
public class YaleAccessor : IYaleAccessor, IDisposable
|
||||
{
|
||||
#region Dispose Logic
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
driver?.Destroy();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
driver = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion Dispose Logic
|
||||
|
||||
private Driver? driver = null;
|
||||
private readonly ZWaveNode lockNode = null!;
|
||||
|
||||
public YaleAccessor(IOptions<ZWaveOptions> zwave, IOptions<DevicesOptions> device)
|
||||
{
|
||||
// Retrive options from configuration
|
||||
ZWaveOptions zwaveOptions = zwave.Value;
|
||||
DevicesOptions devicesOptions = device.Value;
|
||||
|
||||
// Create a new driver instance
|
||||
driver = new Driver(new Uri(zwaveOptions.Url), zwaveOptions.SchemaVersion);
|
||||
|
||||
// Start the driver
|
||||
driver.Start();
|
||||
|
||||
// Flag to indicate if the driver is ready to use
|
||||
bool isReady = false;
|
||||
|
||||
// Subscribe to the driver ready event
|
||||
driver.DriverReady += () =>
|
||||
{
|
||||
isReady = true;
|
||||
};
|
||||
|
||||
driver.StartUpError += (message) =>
|
||||
{
|
||||
throw new Exception(message);
|
||||
};
|
||||
|
||||
// Wait for the driver to be ready
|
||||
while (!isReady)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
// Get the lock node from the driver
|
||||
lockNode = driver.Controller.Nodes.Get(devicesOptions.YaleLockNodeId);
|
||||
}
|
||||
|
||||
public async Task<YaleUserCode> GetCodeInformationAsync(int userCodeId)
|
||||
{
|
||||
// Setup the two tasks to get the values we need
|
||||
CMDResult status = await lockNode.GetValue(GetUserStatusValue(userCodeId));
|
||||
CMDResult code = await lockNode.GetValue(GetUserCodeValue(userCodeId));
|
||||
|
||||
// Covert the result to a YaleUserCode object
|
||||
return new YaleUserCode()
|
||||
{
|
||||
Id = userCodeId,
|
||||
Code = GetUserCodeValue(code),
|
||||
Status = GetUserStatusValue(status),
|
||||
IsHome = false
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> SetUserCode(int userCodeId, string code)
|
||||
{
|
||||
// Setup the set value task
|
||||
CMDResult result = await lockNode.SetValue(GetUserCodeValue(userCodeId), code);
|
||||
|
||||
// If the result is not successful log the message
|
||||
if (!result.Success)
|
||||
{
|
||||
Log.Logger.Error("Failed to set user code {@userCodeId} to {@code}. Error message: {message}", userCodeId, code, result.Message);
|
||||
}
|
||||
|
||||
// Return the result
|
||||
return result.Success;
|
||||
}
|
||||
|
||||
public async Task<bool> SetCodeAsAvailable(int userCode)
|
||||
{
|
||||
// Setup the set value task
|
||||
CMDResult result = await lockNode.SetValue(GetUserStatusValue(userCode), (int)UserCodeStatus.AVAILABLE);
|
||||
|
||||
// If the result is not successful log the message
|
||||
if (!result.Success)
|
||||
{
|
||||
Log.Logger.Error("Failed to set user code {@userCode} to available status. Error message: {message}", userCode, result.Message);
|
||||
}
|
||||
|
||||
// Return the result
|
||||
return result.Success;
|
||||
}
|
||||
|
||||
private static UserCodeStatus GetUserStatusValue(CMDResult result)
|
||||
{
|
||||
// Parse the payload as a JSON object
|
||||
JObject payloadObject = (JObject)result.ResultPayload;
|
||||
|
||||
// Return the value property
|
||||
return (UserCodeStatus)payloadObject.GetValue("value")!.ToObject<int>();
|
||||
}
|
||||
|
||||
private static string GetUserCodeValue(CMDResult result)
|
||||
{
|
||||
// Parse the payload as a JSON object
|
||||
JObject payloadObject = (JObject)result.ResultPayload;
|
||||
|
||||
// Return the value property
|
||||
return payloadObject.GetValue("value")!.ToString();
|
||||
}
|
||||
|
||||
private static ValueID GetUserStatusValue(int userCodeId)
|
||||
{
|
||||
return new ValueID()
|
||||
{
|
||||
commandClass = 99,
|
||||
endpoint = 0,
|
||||
property = "userIdStatus",
|
||||
propertyKey = userCodeId
|
||||
};
|
||||
}
|
||||
|
||||
private static ValueID GetUserCodeValue(int userCodeId)
|
||||
{
|
||||
return new ValueID()
|
||||
{
|
||||
commandClass = 99,
|
||||
endpoint = 0,
|
||||
property = "userCode",
|
||||
propertyKey = userCodeId
|
||||
};
|
||||
}
|
||||
|
||||
public static string ValidateCode(string newCode)
|
||||
{
|
||||
// The code must be between 4 and 6 digits
|
||||
if (newCode.Length < 4 || newCode.Length > 6)
|
||||
{
|
||||
return "The code must be between 4 and 6 digits.";
|
||||
}
|
||||
|
||||
// The code must be numeric
|
||||
if (!int.TryParse(newCode, out int _))
|
||||
{
|
||||
return "The code must be numeric.";
|
||||
}
|
||||
|
||||
// Otherwise, the code is valid, return an empty string
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static string ValidateClearCode(int codeId, int homeCodeId)
|
||||
{
|
||||
// If the code is the home code, return an error message
|
||||
if (codeId == homeCodeId)
|
||||
{
|
||||
return "The home code cannot be cleared.";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
25
packages/backend/Templates/secrets.template.json
Normal file
25
packages/backend/Templates/secrets.template.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"LogLocation": "<Location>/Log.txt", // Location of the log file
|
||||
"CorsAllowedOrigins": "http://localhost:5173", // The URL of the frontend SPA
|
||||
"Devices": {
|
||||
"YaleLockNodeId": "?" // The yale lock node id from your ZWave Network
|
||||
},
|
||||
"Codes": {
|
||||
"Home": 1, // Home user code
|
||||
"GuestCodeRangeStart": 2, // Guest or user codes start ID
|
||||
"GuestCodeRangeCount": 5 // Guest or user codes count
|
||||
},
|
||||
"ZWave": {
|
||||
"Url": "ws://localhost:3000", // The URL of the ZWave Server
|
||||
"SchemaVersion": "?" // The schema version of your ZWave Network
|
||||
},
|
||||
"Authentication": {
|
||||
"Password": "<Password>" // Password to access the YaleAccess Portal
|
||||
},
|
||||
"Twilio": {
|
||||
"AccountSid": "<SID>",
|
||||
"AuthToken": "<Auth>",
|
||||
"FromNumber": "<From Number>",
|
||||
"Message": "<Message>"
|
||||
}
|
||||
}
|
51
packages/backend/YaleAccess.csproj
Normal file
51
packages/backend/YaleAccess.csproj
Normal file
@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>83aa9238-2d6b-483c-b60d-886f32d17532</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>.</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Templates\**" />
|
||||
<Content Remove="Templates\**" />
|
||||
<EmbeddedResource Remove="Templates\**" />
|
||||
<None Remove="Templates\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.20">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Semver" Version="2.3.0" />
|
||||
<PackageReference Include="Serilog" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="5.0.0" />
|
||||
<PackageReference Include="Twilio" Version="7.8.0" />
|
||||
<PackageReference Include="Websocket.Client" Version="4.6.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="lib\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="ZWaveJS.NET">
|
||||
<HintPath>lib\ZWaveJS.NET.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
31
packages/backend/YaleAccess.sln
Normal file
31
packages/backend/YaleAccess.sln
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.7.34221.43
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YaleAccess", "YaleAccess.csproj", "{BC7CD738-F262-484A-9C91-CED5BBBAD2A9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{A93B8970-E2F5-410D-9587-72572C991A61}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\..\README.md = ..\..\README.md
|
||||
Templates\secrets.template.json = Templates\secrets.template.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{BC7CD738-F262-484A-9C91-CED5BBBAD2A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BC7CD738-F262-484A-9C91-CED5BBBAD2A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BC7CD738-F262-484A-9C91-CED5BBBAD2A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BC7CD738-F262-484A-9C91-CED5BBBAD2A9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {6CB96495-C495-41AF-BFAB-329DFC3E350C}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
6
packages/backend/appsettings.Development.json
Normal file
6
packages/backend/appsettings.Development.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"UseMockDevelopmentMode": true,
|
||||
"ConnectionStrings": {
|
||||
"YaleContext": "Data Source=../../development/yale.db"
|
||||
}
|
||||
}
|
9
packages/backend/appsettings.json
Normal file
9
packages/backend/appsettings.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
BIN
packages/backend/lib/ZWaveJS.NET.dll
Normal file
BIN
packages/backend/lib/ZWaveJS.NET.dll
Normal file
Binary file not shown.
25
packages/frontend/.dockerignore
Normal file
25
packages/frontend/.dockerignore
Normal file
@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
24
packages/frontend/.gitignore
vendored
Normal file
24
packages/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
13
packages/frontend/Dockerfile
Normal file
13
packages/frontend/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM node:lts-alpine as build
|
||||
WORKDIR /src
|
||||
COPY ["package.json", "."]
|
||||
COPY ["yarn.lock", "."]
|
||||
RUN yarn install
|
||||
COPY . .
|
||||
RUN yarn build
|
||||
|
||||
FROM node:lts-alpine as production
|
||||
COPY --from=build /src/.output /app
|
||||
WORKDIR /app
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server/index.mjs"]
|
13
packages/frontend/app.vue
Normal file
13
packages/frontend/app.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
bodyAttrs: {
|
||||
class: 'bg-black'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
26
packages/frontend/components/HealthCheck.vue
Normal file
26
packages/frontend/components/HealthCheck.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const healthResult = ref<string>("Fetching health check...");
|
||||
|
||||
onMounted(async () => {
|
||||
// Send a request to the API Health Check endpoint
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/Health`);
|
||||
|
||||
// Extract the response payload (string result)
|
||||
const result = await response.text();
|
||||
|
||||
// Set the result
|
||||
healthResult.value = result;
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-slate-300">
|
||||
<h1>Health Check</h1>
|
||||
<p>{{ healthResult }}</p>
|
||||
</div>
|
||||
</template>
|
67
packages/frontend/components/LoginForm.vue
Normal file
67
packages/frontend/components/LoginForm.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang='ts'>
|
||||
import { ref } from 'vue';
|
||||
import { type ApiResponse } from '~/types/api-response';
|
||||
|
||||
const password = ref('');
|
||||
const passwordError = ref('');
|
||||
const authenticated = useCookie<boolean>('authenticated');
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
const handleLogin = async (event: Event) => {
|
||||
// Prevent default form submission
|
||||
event.preventDefault();
|
||||
|
||||
// Reset the error
|
||||
passwordError.value = '';
|
||||
|
||||
// Send a request to the api to login
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/Authentication/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: `"${password.value}"`
|
||||
});
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<boolean>;
|
||||
|
||||
if (apiResponse.success) {
|
||||
// If the response was successful, login and redirect to the home page
|
||||
authenticated.value = true;
|
||||
navigateTo('/');
|
||||
} else {
|
||||
// Otherwise, set the error
|
||||
passwordError.value = apiResponse.error ?? 'Unknown error';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-zinc-800 text-slate-300 text-center w-full md:w-96 rounded-md">
|
||||
<h1 class="text-7xl mt-5">Yale</h1>
|
||||
<div>
|
||||
<h4 class="hr text-2xl mt-3">User Access</h4>
|
||||
</div>
|
||||
|
||||
<form @submit="handleLogin">
|
||||
<YaleFormInput type="password" v-model="password" placeholder="Password" class="block mt-10 w-5/6 mx-auto" />
|
||||
<p class="mt-3 w-5/6 mx-auto text-red-600" v-if="passwordError">{{ passwordError }}</p>
|
||||
<YaleButton type="submit" class="mb-8 mt-3 w-5/6 mx-auto ">Login</YaleButton>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.hr {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hr::after {
|
||||
content: '';
|
||||
display: block;
|
||||
border-top: 2px solid theme('colors.slate.300');
|
||||
margin-top: .1rem;
|
||||
}
|
||||
</style>
|
37
packages/frontend/components/Modal.vue
Normal file
37
packages/frontend/components/Modal.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void
|
||||
}>();
|
||||
|
||||
const handleClick = () => {
|
||||
emit("close");
|
||||
}
|
||||
|
||||
// If clicking outside the modal, close it
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target === document.querySelector(".fixed")) {
|
||||
handleClick();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center">
|
||||
<div class="bg-zinc-800 rounded-md min-w-96">
|
||||
<div class="p-6 border-b border-slate-300">
|
||||
<h3 class="text-md font-bold flex justify-between items-center">
|
||||
<slot name="title"></slot>
|
||||
<YaleButton type="button" @click="handleClick">
|
||||
<IconXMark />
|
||||
</YaleButton>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6 border-b border-slate-300">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
7
packages/frontend/components/Navbar.vue
Normal file
7
packages/frontend/components/Navbar.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<nav class="w-full bg-zinc-800 p-4 mb-4 flex gap-5 items-center">
|
||||
<h1 class="text-slate-300 font-bold text-2xl inline-block">Yale | User Access</h1>
|
||||
<NuxtLink to="/" class="text-slate-300 hover:text-slate-100">Home</NuxtLink>
|
||||
<NuxtLink to="/people" class="text-slate-300 hover:text-slate-100">People</NuxtLink>
|
||||
</nav>
|
||||
</template>
|
132
packages/frontend/components/PeopleList.vue
Normal file
132
packages/frontend/components/PeopleList.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { type Person } from '@/types/yale';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { ApiResponse } from '~/types/api-response';
|
||||
|
||||
const people = ref([] as Person[]);
|
||||
const person = ref({ id: 0, name: '', phoneNumber: '' } as Person);
|
||||
const loading = ref(true);
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
const addModal = ref(false);
|
||||
|
||||
// When the component is mounted, load the people
|
||||
onMounted(async () => {
|
||||
await loadPeople();
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const loadPeople = async () => {
|
||||
// Send a request to the api to get the people
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/People`, { credentials: 'include' });
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<Person[]>;
|
||||
|
||||
// Set the people
|
||||
people.value = apiResponse.data || [];
|
||||
}
|
||||
|
||||
const handleAddPerson = () => {
|
||||
addModal.value = true;
|
||||
}
|
||||
|
||||
const handleSavePerson = async () => {
|
||||
// Validate the person fields
|
||||
if (!person.value.name || !person.value.phoneNumber) {
|
||||
alert('Name and phone number are required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the loading status
|
||||
loading.value = true;
|
||||
|
||||
// Send a request to the api to save the person
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/People`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(person.value)
|
||||
});
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<Person>;
|
||||
|
||||
if (apiResponse.success) {
|
||||
// Add the new person to the list
|
||||
people.value.push(apiResponse.data!);
|
||||
addModal.value = false;
|
||||
} else {
|
||||
alert(apiResponse.error ?? 'An error occurred');
|
||||
}
|
||||
|
||||
// Close the modal and reset the person
|
||||
loading.value = false;
|
||||
person.value = { id: 0, name: '', phoneNumber: '' };
|
||||
|
||||
// Set the loading status
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const handleDeletePerson = async (id: number) => {
|
||||
// Set the loading status
|
||||
loading.value = true;
|
||||
|
||||
// Send a request to the api to delete the person
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/People/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<Person>;
|
||||
|
||||
if (apiResponse.success) {
|
||||
// Remove the person from the list
|
||||
const index = people.value.findIndex(x => x.id === id);
|
||||
people.value.splice(index, 1);
|
||||
} else {
|
||||
alert(apiResponse.error ?? 'An error occurred');
|
||||
}
|
||||
|
||||
// Set the loading status
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Handle loading state -->
|
||||
<div v-if="loading">Loading...</div>
|
||||
<template v-else>
|
||||
<div class="text-end my-2">
|
||||
<YaleButton type="button" @click="handleAddPerson">Add Person</YaleButton>
|
||||
</div>
|
||||
<table class="w-full text-left">
|
||||
<tr>
|
||||
<th scope="col">Person ID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Phone Number</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
<PeopleListRow v-for="person in people" :key="person.id" :person="person" @delete-person="handleDeletePerson" />
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<!-- Add modal -->
|
||||
<Modal v-if="addModal" @close="addModal = false">
|
||||
<template #title>
|
||||
Add Person
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<YaleFormInput v-model="person.name" type="text" placeholder="Name" class="block w-full" />
|
||||
<YaleFormInput v-model="person.phoneNumber" type="text" placeholder="Phone" class="block w-full" />
|
||||
<div class="flex justify-end">
|
||||
<YaleButton type="button" @click="handleSavePerson">Save</YaleButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
34
packages/frontend/components/PeopleListRow.vue
Normal file
34
packages/frontend/components/PeopleListRow.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup lang='ts'>
|
||||
import { type Person } from '~/types/yale';
|
||||
import { type PropType } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
person: {
|
||||
type: Object as PropType<Person>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete-person", id: number): void
|
||||
}>();
|
||||
|
||||
// Handle the clear code button click
|
||||
const handleDeletePersonClick = () => {
|
||||
// Emit the event to the parent component
|
||||
emit("delete-person", props.person.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr scope="row">
|
||||
<td>{{ person.id }}</td>
|
||||
<td>{{ person.name }}</td>
|
||||
<td>{{ person.phoneNumber }}</td>
|
||||
<td class="flex">
|
||||
<YaleButton type="button" @click="handleDeletePersonClick">
|
||||
<IconTrash />
|
||||
</YaleButton>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
300
packages/frontend/components/UserCodeList.vue
Normal file
300
packages/frontend/components/UserCodeList.vue
Normal file
@ -0,0 +1,300 @@
|
||||
<script setup lang='ts'>
|
||||
import type { ApiResponse } from '@/types/api-response';
|
||||
import UserCodeListRow from './UserCodeListRow.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { UserCodeStatus, type Person, type YaleUserCode } from '@/types/yale';
|
||||
|
||||
const userCodes = ref([] as YaleUserCode[]);
|
||||
const userCode = ref({} as YaleUserCode);
|
||||
const people = ref([] as { label: string, value: string }[]);
|
||||
const personNumber = ref('');
|
||||
const loading = ref(true);
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
// Modals
|
||||
const showModal = ref(false);
|
||||
const editModal = ref(false);
|
||||
const clearModal = ref(false);
|
||||
const sendModal = ref(false);
|
||||
|
||||
// Form Fields
|
||||
const newCode = ref('');
|
||||
const newCodeError = ref('');
|
||||
|
||||
// When the component is mounted, load the user codes
|
||||
onMounted(async () => {
|
||||
// Load the user codes and set the loading status when complete
|
||||
await loadUserCodes();
|
||||
await loadPeople();
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const loadUserCodes = async () => {
|
||||
// Send a request to the api to get the user codes
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/Yale/codes`, { credentials: 'include' });
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<YaleUserCode[]>;
|
||||
|
||||
// Set the user codes by id
|
||||
userCodes.value = apiResponse.data?.sort((a, b) => a.id - b.id) || [];
|
||||
}
|
||||
|
||||
const loadPeople = async () => {
|
||||
// Send a request to the api to get the people
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/People`, { credentials: 'include' });
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<Person[]>;
|
||||
|
||||
// Set the people for drop down
|
||||
people.value = apiResponse.data?.map(x => ({ label: x.name, value: x.phoneNumber })) || [];
|
||||
}
|
||||
|
||||
const confirmUpdateCode = async (id: number, code: string) => {
|
||||
// Reset the error
|
||||
newCodeError.value = '';
|
||||
|
||||
// Validate the new code first
|
||||
if (!/^\d{4,6}$/.test(code)) {
|
||||
newCodeError.value = 'Code must be 4, 5 or 6 digits long';
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the loading status
|
||||
loading.value = true;
|
||||
|
||||
// Send a request to the api, the body will just be the new code
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/Yale/code/${id}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: `"${code}"`
|
||||
});
|
||||
|
||||
// Parse the result
|
||||
const apiResponse = await response.json() as ApiResponse<boolean>;
|
||||
|
||||
if (apiResponse.success) {
|
||||
// If the response was successful update the code in the list
|
||||
const index = userCodes.value.findIndex(x => x.id === id);
|
||||
userCodes.value[index].code = code;
|
||||
userCodes.value[index].status = UserCodeStatus.ENABLED;
|
||||
} else {
|
||||
// Otherwise display an error to the user.
|
||||
alert(apiResponse.error ?? 'Unknown error');
|
||||
}
|
||||
|
||||
// Reset the new code
|
||||
newCode.value = '';
|
||||
|
||||
// Close the edit modal
|
||||
editModal.value = false;
|
||||
|
||||
// Set the loading status
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const confirmClearCode = async (id: number) => {
|
||||
// Set the loading status
|
||||
loading.value = true;
|
||||
|
||||
// Send a request to the api
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/Yale/code/${id}/status`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<boolean>;
|
||||
|
||||
if (apiResponse.success) {
|
||||
// If the response was successful set the code to available in the list
|
||||
const index = userCodes.value.findIndex(x => x.id === id);
|
||||
userCodes.value[index].code = '';
|
||||
userCodes.value[index].status = UserCodeStatus.AVAILABLE;
|
||||
} else {
|
||||
// Otherwise display an error to the user.
|
||||
alert(apiResponse.error ?? 'Unknown error');
|
||||
}
|
||||
|
||||
// Close the clear modal
|
||||
clearModal.value = false;
|
||||
|
||||
// Set the loading status
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const handleShowCode = (id: number) => {
|
||||
// Get the user code to show
|
||||
const code = userCodes.value.find(x => x.id === id);
|
||||
|
||||
// If the code is not found then show an error to the user
|
||||
if (!code) {
|
||||
alert('Code not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the user code to show
|
||||
userCode.value = code;
|
||||
|
||||
// Show the show-modal
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
const handleUpdateCode = (id: number) => {
|
||||
// Get the user code to update
|
||||
const code = userCodes.value.find(x => x.id === id);
|
||||
|
||||
// If the code is not found then show an error to the user
|
||||
if (!code) {
|
||||
alert('Code not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the user code to update
|
||||
userCode.value = code;
|
||||
|
||||
// Show the edit-modal
|
||||
editModal.value = true;
|
||||
}
|
||||
|
||||
const handleClearCode = (id: number) => {
|
||||
// Get the user code to clear
|
||||
const code = userCodes.value.find(x => x.id === id);
|
||||
|
||||
// If the code is not found then show an error to the user
|
||||
if (!code) {
|
||||
alert('Code not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the user code to clear
|
||||
userCode.value = code;
|
||||
|
||||
// Show the clear-modal
|
||||
clearModal.value = true;
|
||||
}
|
||||
|
||||
const handleSendCode = (id: number) => {
|
||||
// Get the user code to send
|
||||
const code = userCodes.value.find(x => x.id === id);
|
||||
|
||||
// If the code is not found then show an error to the user
|
||||
if (!code) {
|
||||
alert('Code not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the user code
|
||||
userCode.value = code;
|
||||
|
||||
// Show the send modal
|
||||
sendModal.value = true;
|
||||
}
|
||||
|
||||
const confirmSendCode = async (id: number) => {
|
||||
// Set the loading status
|
||||
loading.value = true;
|
||||
|
||||
if (!personNumber.value) {
|
||||
alert('Please select a person to send the code to');
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a request to the api to send the code
|
||||
const response = await fetch(`${runtimeConfig.public.apiBaseUrl}/api/Yale/code/${id}/send`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: `"${personNumber.value}"`
|
||||
});
|
||||
|
||||
// Parse the response
|
||||
const apiResponse = await response.json() as ApiResponse<boolean>;
|
||||
|
||||
if (apiResponse.success) {
|
||||
alert('Code sent successfully');
|
||||
} else {
|
||||
alert(apiResponse.error ?? 'Unknown error');
|
||||
}
|
||||
|
||||
// Close the clear modal
|
||||
sendModal.value = false;
|
||||
|
||||
// Clear the person number and user code
|
||||
personNumber.value = '';
|
||||
userCode.value = {} as YaleUserCode;
|
||||
|
||||
// Set the loading status
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Handle loading state -->
|
||||
<div v-if="loading">Loading...</div>
|
||||
<table v-else class="w-full text-left">
|
||||
<tr>
|
||||
<th scope="col">Code ID</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
<UserCodeListRow v-for="userCode in userCodes" :key="userCode.id" :user-code="userCode"
|
||||
@update-code="handleUpdateCode" @clear-code="handleClearCode" @show-code="handleShowCode"
|
||||
@send-code="handleSendCode" />
|
||||
</table>
|
||||
|
||||
<!-- Show modal -->
|
||||
<Modal v-if="showModal" @close="showModal = false">
|
||||
<template #title>
|
||||
User Code # {{ userCode.id }}
|
||||
</template>
|
||||
<template #default>
|
||||
Code: {{ userCode.code }}
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- Edit modal -->
|
||||
<Modal v-if="editModal" @close="editModal = false">
|
||||
<template #title>
|
||||
Edit User Code # {{ userCode.id }}
|
||||
</template>
|
||||
<template #default>
|
||||
<YaleFormInput v-model="newCode" type="text" placeholder="New Code" class="block w-full" />
|
||||
<p class="text-red-600 mt-3" v-if="newCodeError">{{ newCodeError }}</p>
|
||||
<YaleButton @click="confirmUpdateCode(userCode.id, newCode)" class="w-full mt-3">Update Code</YaleButton>
|
||||
</template>
|
||||
<template #footer v-if="userCode.isHome">
|
||||
<p class="text-red-600 mt-3">You are about to edit the home code, are you sure?</p>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- Clear modal -->
|
||||
<Modal v-if="clearModal" @close="clearModal = false">
|
||||
<template #title>
|
||||
Delete User Code # {{ userCode.id }}
|
||||
</template>
|
||||
<template #default>
|
||||
<p>Are you sure you want to delete this code?</p>
|
||||
<YaleButton @click="confirmClearCode(userCode.id)" class="w-full mt-3">Delete Code</YaleButton>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- Send modal -->
|
||||
<Modal v-if="sendModal" @close="sendModal = false">
|
||||
<template #title>
|
||||
Send User Code # {{ userCode.id }}
|
||||
</template>
|
||||
<template #default>
|
||||
<YaleFormSelect v-model="personNumber" :options="people" class="block w-full"
|
||||
placeholder="Select a Person" />
|
||||
<YaleButton @click="confirmSendCode(userCode.id)" class="w-full mt-3">Send Code</YaleButton>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
82
packages/frontend/components/UserCodeListRow.vue
Normal file
82
packages/frontend/components/UserCodeListRow.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script setup lang='ts'>
|
||||
import { UserCodeStatus, type YaleUserCode } from '~/types/yale';
|
||||
import { type PropType } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
userCode: {
|
||||
type: Object as PropType<YaleUserCode>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "show-code", id: number): void
|
||||
(e: "update-code", id: number): void
|
||||
(e: "clear-code", id: number): void
|
||||
(e: "send-code", id: number): void
|
||||
}>();
|
||||
|
||||
const handleShowCodeClick = () => {
|
||||
// Emit the event to the parent component to handle
|
||||
emit("show-code", props.userCode.id);
|
||||
}
|
||||
|
||||
// Handle the update code button click
|
||||
const handleUpdateCodeClick = () => {
|
||||
// Emit the event to the parent component
|
||||
emit("update-code", props.userCode.id);
|
||||
}
|
||||
|
||||
// Handle the clear code button click
|
||||
const handleClearCodeClick = () => {
|
||||
// Emit the event to the parent component
|
||||
emit("clear-code", props.userCode.id);
|
||||
}
|
||||
|
||||
// Handle the send code button click
|
||||
const handleSendCodeClick = () => {
|
||||
// Emit the event to the parent component
|
||||
emit("send-code", props.userCode.id);
|
||||
}
|
||||
|
||||
const userCodeStatusDisplay = (status: UserCodeStatus): string => {
|
||||
switch (status) {
|
||||
case UserCodeStatus.AVAILABLE:
|
||||
return 'Available';
|
||||
case UserCodeStatus.ENABLED:
|
||||
return 'Enabled';
|
||||
case UserCodeStatus.DISABLED:
|
||||
return 'Disabled';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr scope="row">
|
||||
<td>
|
||||
<template v-if="userCode.isHome">
|
||||
{{ userCode.id }} (<IconHome />)
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ userCode.id }}
|
||||
</template>
|
||||
</td>
|
||||
<td>{{ userCodeStatusDisplay(userCode.status) }}</td>
|
||||
<td class="flex">
|
||||
<YaleButton type="button" @click="handleShowCodeClick" :disabled="props.userCode.status !== UserCodeStatus.ENABLED">
|
||||
<IconEye />
|
||||
</YaleButton>
|
||||
<YaleButton type="button" class="ml-2" @click="handleUpdateCodeClick">
|
||||
<IconPencil />
|
||||
</YaleButton>
|
||||
<YaleButton type="button" class="ml-2" @click="handleSendCodeClick" :disabled="props.userCode.status !== UserCodeStatus.ENABLED" v-if="!props.userCode.isHome">
|
||||
<IconSend />
|
||||
</YaleButton>
|
||||
<YaleButton type="button" class="ml-2" @click="handleClearCodeClick" :disabled="props.userCode.status !== UserCodeStatus.ENABLED" v-if="!props.userCode.isHome">
|
||||
<IconTrash />
|
||||
</YaleButton>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
7
packages/frontend/components/icon/eye-slash.vue
Normal file
7
packages/frontend/components/icon/eye-slash.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</template>
|
8
packages/frontend/components/icon/eye.vue
Normal file
8
packages/frontend/components/icon/eye.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</template>
|
7
packages/frontend/components/icon/home.vue
Normal file
7
packages/frontend/components/icon/home.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6 inline">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
</template>
|
7
packages/frontend/components/icon/pencil.vue
Normal file
7
packages/frontend/components/icon/pencil.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
||||
</svg>
|
||||
</template>
|
7
packages/frontend/components/icon/send.vue
Normal file
7
packages/frontend/components/icon/send.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-send"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M15.854.146a.5.5 0 0 1 .11.54l-5.819 14.547a.75.75 0 0 1-1.329.124l-3.178-4.995L.643 7.184a.75.75 0 0 1 .124-1.33L15.314.037a.5.5 0 0 1 .54.11ZM6.636 10.07l2.761 4.338L14.13 2.576zm6.787-8.201L1.591 6.602l4.339 2.76z" />
|
||||
</svg>
|
||||
</template>
|
7
packages/frontend/components/icon/trash.vue
Normal file
7
packages/frontend/components/icon/trash.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</template>
|
6
packages/frontend/components/icon/x-mark.vue
Normal file
6
packages/frontend/components/icon/x-mark.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</template>
|
28
packages/frontend/components/yale/Button.vue
Normal file
28
packages/frontend/components/yale/Button.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLButtonTypes } from '~/types/html-input-types';
|
||||
|
||||
interface ButtonProps {
|
||||
type?: HTMLButtonTypes
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
type: 'button',
|
||||
disabled: false
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'click'
|
||||
])
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :type="props.type" :disabled="props.disabled" class="bg-stone-950 hover:bg-stone-900 p-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="handleClick">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
25
packages/frontend/components/yale/FormInput.vue
Normal file
25
packages/frontend/components/yale/FormInput.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLInputTypes } from '~/types/html-input-types';
|
||||
|
||||
const props = defineProps<{
|
||||
type: HTMLInputTypes
|
||||
placeholder: string
|
||||
modelValue: string
|
||||
}>();
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
// If input is a text input, emit the value
|
||||
if (props.type === 'text' || props.type === 'password') {
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input class="bg-zinc-900 p-2 rounded-md" :type="props.type"
|
||||
:value="props.modelValue" :placeholder="props.placeholder" @input="handleInput">
|
||||
</template>~/types/html-input-types
|
27
packages/frontend/components/yale/FormSelect.vue
Normal file
27
packages/frontend/components/yale/FormSelect.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
options: Array<{ label: string; value: string }>,
|
||||
placeholder: string,
|
||||
modelValue: string
|
||||
}>();
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const handleSelect = (event: Event) => {
|
||||
emit('update:modelValue', (event.target as HTMLSelectElement).value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select class="bg-zinc-900 p-2 rounded-md" :value="props.modelValue" @change="handleSelect">
|
||||
<!-- Placeholder as the first option -->
|
||||
<option value="" disabled selected>{{ props.placeholder }}</option>
|
||||
|
||||
<!-- Render options dynamically -->
|
||||
<option v-for="option in props.options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
25
packages/frontend/components/yale/Panel.vue
Normal file
25
packages/frontend/components/yale/Panel.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
heading?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 v-if="props.heading" class="hr text-lg font-bold mb-2">{{ props.heading }}</h2>
|
||||
<div class="rounded-md bg-zinc-800 p-2">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.hr {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hr::after {
|
||||
content: '';
|
||||
display: block;
|
||||
border-top: 2px solid theme('colors.slate.300');
|
||||
margin-top: .1rem;
|
||||
}
|
||||
</style>
|
5
packages/frontend/layouts/blank.vue
Normal file
5
packages/frontend/layouts/blank.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="text-slate-300">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
6
packages/frontend/layouts/default.vue
Normal file
6
packages/frontend/layouts/default.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<Navbar />
|
||||
<div class="text-slate-300 container mx-auto px-4">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
9
packages/frontend/middleware/authentication.global.ts
Normal file
9
packages/frontend/middleware/authentication.global.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const anonymousRoutes = ['/login', '/health'];
|
||||
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// If the route is not the login page and the user is not logged in then redirect to login
|
||||
const authenticated = useCookie<boolean>('authenticated');
|
||||
if (!anonymousRoutes.includes(to.path) && !(authenticated.value) ) {
|
||||
return navigateTo('/login');
|
||||
}
|
||||
})
|
10
packages/frontend/nuxt.config.ts
Normal file
10
packages/frontend/nuxt.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
modules: [ '@nuxtjs/tailwindcss' ],
|
||||
devtools: { enabled: true },
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBaseUrl: 'https://localhost:7069'
|
||||
}
|
||||
}
|
||||
})
|
20
packages/frontend/package.json
Normal file
20
packages/frontend/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/devtools": "latest",
|
||||
"@nuxtjs/tailwindcss": "^6.11.3",
|
||||
"nuxt": "^3.9.1",
|
||||
"vue": "^3.4.10",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
3
packages/frontend/pages/health.vue
Normal file
3
packages/frontend/pages/health.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<HealthCheck />
|
||||
</template>
|
5
packages/frontend/pages/index.vue
Normal file
5
packages/frontend/pages/index.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<YalePanel heading="User Code List">
|
||||
<UserCodeList />
|
||||
</YalePanel>
|
||||
</template>
|
11
packages/frontend/pages/login.vue
Normal file
11
packages/frontend/pages/login.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'blank'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex items-center justify-center w-11/12 md:w-auto mx-auto">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</template>
|
5
packages/frontend/pages/people.vue
Normal file
5
packages/frontend/pages/people.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<YalePanel title="People">
|
||||
<PeopleList />
|
||||
</YalePanel>
|
||||
</template>
|
BIN
packages/frontend/public/favicon.ico
Normal file
BIN
packages/frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
3
packages/frontend/server/tsconfig.json
Normal file
3
packages/frontend/server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
4
packages/frontend/tsconfig.json
Normal file
4
packages/frontend/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
5
packages/frontend/types/api-response.ts
Normal file
5
packages/frontend/types/api-response.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type ApiResponse<T> = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
data?: T;
|
||||
}
|
3
packages/frontend/types/html-input-types.ts
Normal file
3
packages/frontend/types/html-input-types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type HTMLInputTypes = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'time' | 'datetime-local' | 'month' | 'week' | 'color';
|
||||
|
||||
export type HTMLButtonTypes = 'submit' | 'reset' | 'button';
|
18
packages/frontend/types/yale.ts
Normal file
18
packages/frontend/types/yale.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export type YaleUserCode = {
|
||||
id: number;
|
||||
code: string;
|
||||
status: UserCodeStatus;
|
||||
isHome: boolean;
|
||||
}
|
||||
|
||||
export enum UserCodeStatus {
|
||||
AVAILABLE = 0,
|
||||
ENABLED = 1,
|
||||
DISABLED = 2
|
||||
}
|
||||
|
||||
export type Person = {
|
||||
id: number;
|
||||
name: string;
|
||||
phoneNumber: string;
|
||||
}
|
7096
packages/frontend/yarn.lock
Normal file
7096
packages/frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
37
sample.compose.yml
Normal file
37
sample.compose.yml
Normal file
@ -0,0 +1,37 @@
|
||||
services:
|
||||
|
||||
frontend:
|
||||
container_name: yale-access-frontend
|
||||
image: liamp1/yale-access-frontend:latest
|
||||
restart: unless-stopped
|
||||
|
||||
ports: 5015:3000
|
||||
|
||||
environment:
|
||||
- NUXT_PUBLIC_API_BASE_URL=http://<computer-ip>:5016
|
||||
|
||||
backend:
|
||||
container_name: yale-access-backend
|
||||
image: liamp1/yale-access-backend:latest
|
||||
restart: unless-stopped
|
||||
|
||||
ports: 5016:80
|
||||
|
||||
environment:
|
||||
- LogLocation=/log/log.txt
|
||||
- CorsAllowedOrigins=http://<computer-ip>:5015
|
||||
- Devices__YaleLockNodeId=<node-id>
|
||||
- Codes__Home=<code-id>
|
||||
- Codes__GuestCodeRangeStart=<code-id>
|
||||
- Codes__GuestCodeRangeCount=<count>
|
||||
- ZWave__Url=http://<z-wave-ip>:<z-wave-port>
|
||||
- ZWave__SchemaVersion=<z-wave-schema>
|
||||
- Authentication__Password=<password>
|
||||
- Twilio__AccountSid=<twilio-account-sid>
|
||||
- Twilio__AuthToken=<twilio-auth-token>
|
||||
- Twilio__FromNumber=<twilio-from-number>
|
||||
- Twilio__Message=<twilio-message>
|
||||
|
||||
volumes:
|
||||
- ./log:/log
|
||||
- ./data:/yale-data
|
BIN
screenshots/delete.png
Normal file
BIN
screenshots/delete.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
BIN
screenshots/edit.png
Normal file
BIN
screenshots/edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
BIN
screenshots/home.png
Normal file
BIN
screenshots/home.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
BIN
screenshots/login.png
Normal file
BIN
screenshots/login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
Loading…
Reference in New Issue
Block a user