diff --git a/AH-UN Schedule API/alembic/script.py.mako b/AH-UN Schedule API/alembic/script.py.mako index fbc4b07..6ce3351 100644 --- a/AH-UN Schedule API/alembic/script.py.mako +++ b/AH-UN Schedule API/alembic/script.py.mako @@ -9,6 +9,7 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa +import sqlmodel ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/AH-UN Schedule API/alembic/versions/23f9ecc893dc_userdevice.py b/AH-UN Schedule API/alembic/versions/23f9ecc893dc_userdevice.py new file mode 100644 index 0000000..477128d --- /dev/null +++ b/AH-UN Schedule API/alembic/versions/23f9ecc893dc_userdevice.py @@ -0,0 +1,34 @@ +"""UserDevice + +Revision ID: 23f9ecc893dc +Revises: bfd7738a6d20 +Create Date: 2025-01-05 23:21:39.672449 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision: str = '23f9ecc893dc' +down_revision: Union[str, None] = 'bfd7738a6d20' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('userdevice', + sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('device', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('username', 'device') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('userdevice') + # ### end Alembic commands ### diff --git a/AH-UN Schedule API/alembic/versions/dcdd23261ccf_userdevice_relation.py b/AH-UN Schedule API/alembic/versions/dcdd23261ccf_userdevice_relation.py new file mode 100644 index 0000000..19156aa --- /dev/null +++ b/AH-UN Schedule API/alembic/versions/dcdd23261ccf_userdevice_relation.py @@ -0,0 +1,31 @@ +"""UserDevice_relation + +Revision ID: dcdd23261ccf +Revises: 23f9ecc893dc +Create Date: 2025-01-05 23:23:42.070271 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = 'dcdd23261ccf' +down_revision: Union[str, None] = '23f9ecc893dc' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key(None, 'userdevice', 'user', ['username'], ['username']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'userdevice', type_='foreignkey') + # ### end Alembic commands ### diff --git a/AH-UN Schedule API/src/models/user.py b/AH-UN Schedule API/src/models/user.py index 588035e..b240259 100644 --- a/AH-UN Schedule API/src/models/user.py +++ b/AH-UN Schedule API/src/models/user.py @@ -13,6 +13,7 @@ class User(UserBase, table=True): admin: bool = Field() shifts: list["Shift"] = Relationship(back_populates="user", sa_relationship_kwargs={"cascade": "all, delete-orphan"}) + devices: list["UserDevice"] = Relationship(back_populates="user", sa_relationship_kwargs={"cascade": "all, delete-orphan"}) def serialize(self) -> "UserRead": u = self.model_dump() @@ -28,3 +29,4 @@ class UserUpdate(SQLModel): from src.models.shift import Shift +from src.models.user_device import UserDevice diff --git a/AH-UN Schedule API/src/models/user_device.py b/AH-UN Schedule API/src/models/user_device.py new file mode 100644 index 0000000..bbbeaa2 --- /dev/null +++ b/AH-UN Schedule API/src/models/user_device.py @@ -0,0 +1,15 @@ +from sqlmodel import Relationship, SQLModel, Field + +class UserDeviceBase(SQLModel): + username: str + device: str + + +class UserDevice(UserDeviceBase, table=True): + username: str = Field(primary_key=True, foreign_key="user.username") + device: str = Field(primary_key=True) + + user: "User" = Relationship(back_populates="devices") + + +from src.models.user import User diff --git a/AH-UN Schedule API/src/routers/users.py b/AH-UN Schedule API/src/routers/users.py index 47e354a..2f35f0d 100644 --- a/AH-UN Schedule API/src/routers/users.py +++ b/AH-UN Schedule API/src/routers/users.py @@ -1,10 +1,11 @@ -from fastapi import APIRouter, Depends, HTTPException -from fastapi.responses import JSONResponse +from fastapi import APIRouter, Depends, HTTPException, Response from sqlmodel import Session, select from src.database import get_session from src.middleware.auth import require_auth from src.models.user import User, UserRead, UserUpdate +from src.models.user_device import UserDevice +from src.types import DeviceInfo router = APIRouter( @@ -85,4 +86,35 @@ async def delete_user( session.delete(user) - return JSONResponse(content=None, status_code=200) + return Response(status_code=200) + + +@router.post("/{username}/devices") +async def add_device( + username: str, + info: DeviceInfo, + session: Session = Depends(get_session), + user: User = Depends(require_auth), +): + if username != user.username: + raise HTTPException(status_code=403) + + device = session.exec( + select(UserDevice) + .where( + UserDevice.username == user.username, + UserDevice.device == info.device + ) + ).one_or_none() + + if device: + raise HTTPException(status_code=409) + + device = UserDevice( + username=user.username, + device=info.device + ) + + session.add(device) + + return Response(status_code=201) diff --git a/AH-UN Schedule API/src/types.py b/AH-UN Schedule API/src/types.py index ef35100..d983704 100644 --- a/AH-UN Schedule API/src/types.py +++ b/AH-UN Schedule API/src/types.py @@ -1,5 +1,6 @@ from sqlmodel import SQLModel + class LoginCredentials(SQLModel): username: str password: str @@ -7,3 +8,7 @@ class LoginCredentials(SQLModel): class ImageData(SQLModel): image: str + + +class DeviceInfo(SQLModel): + device: str diff --git a/AH-UN Schedule API/test b/AH-UN Schedule API/test deleted file mode 100644 index 4e1dea2..0000000 --- a/AH-UN Schedule API/test +++ /dev/null @@ -1 +0,0 @@ -$2b$12$H5jKeCrI13G5b1LK1vSDDOvWVUgLIsG2ALNs9l9x0a2C4FhJCh5H. \ No newline at end of file diff --git a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule.xcodeproj/project.pbxproj b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule.xcodeproj/project.pbxproj index 5f7a661..df926e9 100644 --- a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule.xcodeproj/project.pbxproj +++ b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ 813D753B2BACEA37005CC6EA /* Requests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Requests.swift; sourceTree = ""; }; 813D753D2BADB911005CC6EA /* EditUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditUserView.swift; sourceTree = ""; }; 813D75442BB0830A005CC6EA /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 81482AE92D2B3AF9008C1278 /* AH-UN ScheduleDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "AH-UN ScheduleDebug.entitlements"; sourceTree = ""; }; 8196E4B62BA9CE80004FFD5C /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 8196E4B82BAAE111004FFD5C /* EditShiftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditShiftView.swift; sourceTree = ""; }; 81AF41192BB46597000DB01C /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; @@ -125,6 +126,7 @@ 812EEEE02BA383550034E576 /* AH-UN Schedule */ = { isa = PBXGroup; children = ( + 81482AE92D2B3AF9008C1278 /* AH-UN ScheduleDebug.entitlements */, 812EEEE12BA383550034E576 /* AH_UN_ScheduleApp.swift */, 81CCABB42BA5F19B00332A1D /* AH-UN-Schedule-Info.plist */, 81CCD5DE2BA63DBA00EFB5D2 /* PrivacyInfo.xcprivacy */, @@ -489,6 +491,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "AH-UN Schedule/AH-UN ScheduleDebug.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AH-UN Schedule/Preview Content\""; @@ -527,6 +530,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "AH-UN Schedule/AH-UN ScheduleRelease.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AH-UN Schedule/Preview Content\""; @@ -550,6 +555,7 @@ MARKETING_VERSION = 1.5; PRODUCT_BUNDLE_IDENTIFIER = "com.ZoutigeWolf.AH-UN-Schedule"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; diff --git a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH-UN ScheduleDebug.entitlements b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH-UN ScheduleDebug.entitlements new file mode 100644 index 0000000..31c7e99 --- /dev/null +++ b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH-UN ScheduleDebug.entitlements @@ -0,0 +1,10 @@ + + + + + aps-environment + development + com.apple.developer.usernotifications.time-sensitive + + + diff --git a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH-UN ScheduleRelease.entitlements b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH-UN ScheduleRelease.entitlements new file mode 100644 index 0000000..31c7e99 --- /dev/null +++ b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH-UN ScheduleRelease.entitlements @@ -0,0 +1,10 @@ + + + + + aps-environment + development + com.apple.developer.usernotifications.time-sensitive + + + diff --git a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH_UN_ScheduleApp.swift b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH_UN_ScheduleApp.swift index fbe842e..bc29fe9 100644 --- a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH_UN_ScheduleApp.swift +++ b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/AH_UN_ScheduleApp.swift @@ -7,8 +7,11 @@ import SwiftUI + @main struct AH_UN_ScheduleApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @ObservedObject var authentication = AuthManager.shared var body: some Scene { @@ -54,6 +57,20 @@ struct HomeView: View { } } +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceTokenData: Data) { + let token = deviceTokenData.map { String(format: "%02x", $0) }.joined() + + print("Device Token: \(token)") + + UserManager.registerDevice(device: token) { res in } + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + print("Failed to register for remote notifications: \(error.localizedDescription)") + } +} + #Preview { HomeView() } diff --git a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/AuthManager.swift b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/AuthManager.swift index ba62bcf..8ace7d8 100644 --- a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/AuthManager.swift +++ b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/AuthManager.swift @@ -6,12 +6,14 @@ // import Foundation +import SwiftUI +import UserNotifications class AuthManager: ObservableObject { public static let shared = AuthManager() -// static let serverUrl: String = "https://ah-un.zoutigewolf.dev" - static let serverUrl: String = "http://localhost:8000" + static let serverUrl: String = "https://ah-un.zoutigewolf.dev" +// static let serverUrl: String = "http://localhost:8000" @Published var user: User? var token: String? @@ -57,6 +59,14 @@ class AuthManager: ObservableObject { self.getUser() { res in completion(res) } + + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, err in + if granted { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } + } } } } diff --git a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/UserManager.swift b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/UserManager.swift index 8c60958..1be7329 100644 --- a/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/UserManager.swift +++ b/AH-UN Schedule/AH-UN Schedule/AH-UN Schedule/API/UserManager.swift @@ -55,4 +55,23 @@ class UserManager { } } } + + static func registerDevice(device: String, completion: @escaping (Bool) -> Void) { + struct _DeviceInfo: Codable { + var device: String + } + + guard let username = AuthManager.shared.user?.username else { + completion(false) + return + } + + Requests.post(url: AuthManager.serverUrl + "/users/" + username + "/devices", token: AuthManager.shared.token, data: _DeviceInfo(device: device)) { (res: Result) in + switch res { + case .failure(let error): + completion(false) + case .success(_): + completion(true) + } + } } }