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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ dependencies {
implementation 'com.google.firebase:firebase-auth'
implementation 'com.google.firebase:firebase-messaging'
implementation("com.google.firebase:firebase-analytics")
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}

apollo {
Expand Down
17 changes: 15 additions & 2 deletions app/src/main/graphql/User.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ query getUserByNetId($netId: String!) {
}
}

mutation SetWorkoutGoals($id: Int!, $workoutGoal: [String!]!) {
mutation SetWorkoutGoals($id: Int!, $workoutGoal: Int!) {
setWorkoutGoals(userId: $id, workoutGoal: $workoutGoal) {
...userFields
}
Expand All @@ -43,4 +43,17 @@ mutation LogWorkout($facilityId: Int!, $workoutTime: DateTime!, $id: Int!) {
logWorkout(facilityId: $facilityId, userId: $id, workoutTime: $workoutTime) {
...workoutFields
}
}
}

mutation DeleteUser($userId: Int!) {
deleteUser(userId: $userId) {
...userFields
}
}

mutation LoginUser($netId: String!) {
loginUser(netId: $netId) {
accessToken
refreshToken
}
}
93 changes: 60 additions & 33 deletions app/src/main/graphql/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,6 @@ type Query {
"""
getAllReports: [Report]

"""
Get the workout goals of a user by ID.
"""
getWorkoutGoals(id: Int!): [String]

"""
Get the current and max workout streak of a user.
"""
getUserStreak(id: Int!): JSONString

"""
Get all facility hourly average capacities.
"""
Expand Down Expand Up @@ -358,39 +348,39 @@ type User {

name: String!

activeStreak: Int
activeStreak: Int!

maxStreak: Int!

workoutGoal: Int

maxStreak: Int
lastGoalChange: DateTime

workoutGoal: [DayOfWeekGraphQLEnum]
lastStreak: Int!

encodedImage: String

giveaways: [Giveaway]

goalHistory: [WorkoutGoalHistory]

friendRequestsSent: [Friendship]

friendRequestsReceived: [Friendship]

friendships: [Friendship]

friends: [User]
}

enum DayOfWeekGraphQLEnum {
MONDAY

TUESDAY

WEDNESDAY

THURSDAY

FRIDAY

SATURDAY
"""
Get the total number of gym days (unique workout days) for user.
"""
totalGymDays: Int!

SUNDAY
"""
The start date of the most recent active streak, up until the current date.
"""
streakStart: Date
}

type Giveaway {
Expand All @@ -401,6 +391,18 @@ type Giveaway {
users: [User]
}

type WorkoutGoalHistory {
id: ID!

userId: Int!

workoutGoal: Int!

effectiveAt: DateTime!

user: User
}

type Friendship {
id: ID!

Expand All @@ -419,6 +421,13 @@ type Friendship {
friend: User
}

"""
The `Date` scalar type represents a Date
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar Date

type Workout {
id: ID!

Expand All @@ -427,12 +436,9 @@ type Workout {
userId: Int!

facilityId: Int!
}

"""
JSON String
"""
scalar JSONString
gymName: String!
}

type HourlyAverageCapacity {
id: ID!
Expand All @@ -448,6 +454,22 @@ type HourlyAverageCapacity {
history: [Float]!
}

enum DayOfWeekGraphQLEnum {
MONDAY

TUESDAY

WEDNESDAY

THURSDAY

FRIDAY

SATURDAY

SUNDAY
}

type CapacityReminder {
id: ID!

Expand Down Expand Up @@ -518,7 +540,7 @@ type Mutation {
"""
Set a user's workout goals.
"""
setWorkoutGoals("The ID of the user." userId: Int!, "The new workout goal for the user in terms of days of the week." workoutGoal: [String]!): User
setWorkoutGoals("The ID of the user." userId: Int!, "The new workout goal for the user in terms of number of days per week." workoutGoal: Int!): User

"""
Log a user's workout.
Expand All @@ -545,6 +567,11 @@ type Mutation {
"""
createReport(createdAt: DateTime!, description: String!, gymId: Int!, issue: String!): CreateReport

"""
Deletes a report by ID.
"""
deleteReport(reportId: Int!): Report

"""
Deletes a user by ID.
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.cornellappdev.uplift.data.repositories

import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class AuthInterceptor @Inject constructor(
private val tokenManager: TokenManager
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenManager.getAccessToken()
val request = chain.request().newBuilder().apply {
if (token != null) {
addHeader("Authorization", "Bearer $token")
}
}.build()
return chain.proceed(request)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object PreferencesKeys {
val USERNAME = stringPreferencesKey("username")
val NETID = stringPreferencesKey("netId")
val EMAIL = stringPreferencesKey("email")
val GOAL = intPreferencesKey("workoutGoal")
val SKIP = booleanPreferencesKey("skip")
val FCM_TOKEN = stringPreferencesKey("fcmToken")
val DECLINED_NOTIFICATION_PERMISSION =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.cornellappdev.uplift.data.repositories

import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TokenManager @Inject constructor(@ApplicationContext private val context: Context) {
private val fileName = "encrypted_tokens"

// Initialize EncryptedSharedPreferences
private val sharedPreferences: SharedPreferences? by lazy {
try {
createEncryptedPrefs()
} catch (e: Exception) {
Log.e("TokenManager", "Failed to initialize EncryptedSharedPreferences", e)
// Clear corrupted state
context.deleteSharedPreferences(fileName)
try {
// Could have failed due to previous corrupted state
// One more attempt after cleaning the corruption
createEncryptedPrefs()
} catch (retryException: Exception) {
// Probably broken return null
Log.e("TokenManager", "Failed to initialize EncryptedSharedPreferences again", retryException)
null
}
}
Comment on lines +21 to +34
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TokenManager falls back to plain SharedPreferences when EncryptedSharedPreferences initialization fails (catch block). That means access/refresh tokens may be stored unencrypted on some devices, defeating the purpose of introducing encrypted storage. Prefer failing closed here (e.g., clear tokens and force re-auth) rather than silently storing tokens in plaintext.

Copilot uses AI. Check for mistakes.
}

private fun createEncryptedPrefs(): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

return EncryptedSharedPreferences.create(
context,
fileName,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

fun saveTokens(accessToken: String, refreshToken: String) {
sharedPreferences?.edit {
putString("access_token", accessToken)
putString("refresh_token", refreshToken)
}
}

fun getAccessToken(): String? = sharedPreferences?.getString("access_token", null)

fun getRefreshToken(): String? = sharedPreferences?.getString("refresh_token", null)

fun clearTokens() {
sharedPreferences?.edit { clear() }
}
}
Loading
Loading