Skip to content

Commit f79ef87

Browse files
authored
Merge pull request #321 from DevKor-github/dev
[codex] Fix multi-device refresh token sessions
2 parents f78917f + 29a6194 commit f79ef87

78 files changed

Lines changed: 3186 additions & 330 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-dev.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,36 @@ jobs:
173173
fi
174174
175175
$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml pull
176+
$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml up -d mysql
177+
178+
DB_ROOT_PASSWORD="$(get_env_value MYSQL_ROOT_PASSWORD)"
179+
DB_NAME="$(get_env_value MYSQL_DATABASE)"
180+
181+
for attempt in $(seq 1 30); do
182+
MYSQL_STATUS="$(sudo docker inspect -f '{{.State.Health.Status}}' ontime-dev-mysql 2>/dev/null || true)"
183+
if [ "$MYSQL_STATUS" = "healthy" ]; then
184+
echo "MySQL container is healthy."
185+
break
186+
fi
187+
echo "Waiting for healthy MySQL status; current status: ${MYSQL_STATUS:-unknown}"
188+
sleep 5
189+
done
190+
191+
MYSQL_STATUS="$(sudo docker inspect -f '{{.State.Health.Status}}' ontime-dev-mysql 2>/dev/null || true)"
192+
[ "$MYSQL_STATUS" = "healthy" ] || fail_deploy "MySQL container did not become healthy."
193+
194+
FAILED_V17_COUNT="$(
195+
sudo docker exec ontime-dev-mysql \
196+
mysql -u root --password="$DB_ROOT_PASSWORD" "$DB_NAME" \
197+
-Nse "SELECT COUNT(*) FROM flyway_schema_history WHERE version = '17' AND success = 0;" 2>/dev/null || echo 0
198+
)"
199+
if [ "$FAILED_V17_COUNT" != "0" ]; then
200+
echo "Repairing failed development Flyway migration V17 before backend startup."
201+
sudo docker exec ontime-dev-mysql \
202+
mysql -u root --password="$DB_ROOT_PASSWORD" "$DB_NAME" \
203+
-e "DROP TABLE IF EXISTS user_refresh_token; DELETE FROM flyway_schema_history WHERE version = '17' AND success = 0;"
204+
fi
205+
176206
$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml up -d --remove-orphans
177207
178208
HEALTHY=false

docs/account-deletion-api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Account deletion hard-deletes the user from OnTime. The request can optionally i
88

99
For Google and Apple social accounts, the backend first tries to revoke the social login token, then deletes the local OnTime account. If provider token revocation fails, the backend logs a warning and still deletes the local OnTime account.
1010

11+
Account deletion also deletes account-scoped settings and preferences, including the analytics preference stored in `user_analytics_preference`. Future user-linked Product Usage Events stop after deletion; historical analytics may be retained only in aggregate or de-identified form under the approved privacy policy and analytics provider configuration.
12+
1113
For release/privacy evidence by data category, see `docs/account-deletion-verification-evidence.md`.
1214

1315
## Authentication

docs/account-deletion-verification-evidence.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ request timestamp, response, owner, and evidence link.
4949
| Access and refresh tokens | `user.access_token`, `user.refresh_token`, `user_device.session_access_token`, `user_device.session_refresh_token` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete and device cascade | Test asserts user and device rows are absent | Backend repo; release env owner TBD |
5050
| Device records and FCM tokens | `user.firebase_token`, `user_device`, `user_device.firebase_token` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete and device cascade | Test asserts user device row is absent | Backend repo; release env owner TBD |
5151
| Alarm settings and alarm status | `user_alarm_setting`, `user_alarm_status` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user/device | Test asserts alarm setting and status rows are absent | Backend repo; release env owner TBD |
52+
| Analytics preference | `user_analytics_preference` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user | Test asserts analytics preference row is absent | Backend repo; release env owner TBD |
5253
| Default preparation settings | `preparation_user` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user | Test asserts preparation user rows are absent | Backend repo; release env owner TBD |
5354
| Schedules | `schedule`, `notification_schedule` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user and schedule | Test asserts schedule and notification schedule rows are absent | Backend repo; release env owner TBD |
5455
| Schedule preparation steps | `preparation_schedule` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from schedule | Test asserts preparation schedule rows are absent | Backend repo; release env owner TBD |

docs/analytics-preference-api.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Analytics Preference API
2+
3+
Issue: #318
4+
5+
## Summary
6+
7+
The analytics preference API stores whether the signed-in account allows optional
8+
Product Usage Events. The preference is account-scoped, not device-scoped.
9+
10+
This API does not define Firebase event names, frontend instrumentation, local
11+
pre-login preference storage, UI copy, marketing analytics, personalization, or
12+
Firebase Remote Config behavior.
13+
14+
## Default And Release Gate
15+
16+
Backend default is controlled by:
17+
18+
```properties
19+
analytics.preference.default-enabled=${ANALYTICS_PREFERENCE_DEFAULT_ENABLED:false}
20+
```
21+
22+
The default remains `false` until the privacy policy and Google Play Data Safety
23+
updates are approved for the Firebase Analytics release. After approval, deploy
24+
owners may set `ANALYTICS_PREFERENCE_DEFAULT_ENABLED=true`.
25+
26+
Rows created before the default is flipped are marked as not user-overridden.
27+
When the service reads a non-overridden row, it may align that row to the current
28+
deploy default. Once a user explicitly updates the preference, the row is marked
29+
as user-overridden and future default flips do not change that choice.
30+
31+
## Get Analytics Preference
32+
33+
```http
34+
GET /users/me/analytics-preference
35+
Authorization: Bearer <access token>
36+
```
37+
38+
Successful response:
39+
40+
```json
41+
{
42+
"status": "success",
43+
"code": 200,
44+
"message": "OK",
45+
"data": {
46+
"enabled": false,
47+
"updatedAt": "2026-05-26T12:00:00Z"
48+
}
49+
}
50+
```
51+
52+
## Update Analytics Preference
53+
54+
```http
55+
PUT /users/me/analytics-preference
56+
Authorization: Bearer <access token>
57+
Content-Type: application/json
58+
59+
{
60+
"enabled": true
61+
}
62+
```
63+
64+
Successful response:
65+
66+
```json
67+
{
68+
"status": "success",
69+
"code": 200,
70+
"message": "OK",
71+
"data": {
72+
"enabled": true,
73+
"updatedAt": "2026-05-26T12:00:05Z"
74+
}
75+
}
76+
```
77+
78+
`enabled` is required and must be a JSON boolean. Missing, null, non-boolean, or
79+
unknown fields are rejected with the existing validation-style `400` response.
80+
81+
## Persistence And Deletion
82+
83+
The preference is stored in `user_analytics_preference` with a unique foreign key
84+
to `user(user_id)`, `enabled`, `updated_at`, and the internal
85+
`user_overridden` flag.
86+
87+
On account deletion, the local analytics preference row is deleted by foreign-key
88+
cascade. Future user-linked Product Usage Events stop when the account
89+
preference is disabled or the account is deleted. Historical analytics may be
90+
retained only in aggregate or de-identified form, subject to the approved privacy
91+
policy and Firebase/analytics project configuration.
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# Preparation Templates Frontend Contract
2+
3+
## Concepts
4+
Schedules now have one preparation source:
5+
6+
- `DEFAULT`: uses the user's fixed default preparation from `GET /users/preparations`.
7+
- `TEMPLATE`: uses a named preparation template from `GET /preparation-templates`.
8+
- `CUSTOM`: uses schedule-specific preparation steps.
9+
10+
Started schedules are frozen. Use `preparationFrozen` from schedule responses; it is `true` when `startedAt` is present. Frozen schedules still show their original `preparationMode`, but their preparation steps come from the schedule snapshot.
11+
12+
## Ordered Preparation Shape
13+
New APIs use ordered steps:
14+
15+
```json
16+
{
17+
"preparationId": "3fa85f64-5717-4562-b3fc-2c963f66afe5",
18+
"preparationName": "Shower",
19+
"preparationTime": 15,
20+
"orderIndex": 0
21+
}
22+
```
23+
24+
Rules:
25+
- Client provides UUIDs for templates and preparation steps.
26+
- `orderIndex` is zero-based and contiguous.
27+
- Arrays may be sent in any order; backend stores and returns by `orderIndex`.
28+
- Each list must contain 1-50 steps.
29+
- Each `preparationTime` must be 1-1440 minutes.
30+
- Total preparation time per list must be at most 1440 minutes.
31+
- Step names are trimmed; duplicate step names are allowed.
32+
- Step IDs must not be reused across other templates, other schedules, or the fixed default. Reusing the same step ID within the same resource update is allowed.
33+
34+
## Named Template APIs
35+
The fixed default preparation is not included in these endpoints.
36+
37+
### List Active Templates
38+
`GET /preparation-templates`
39+
40+
Returns active named templates with full steps. Deleted templates are excluded.
41+
42+
```json
43+
{
44+
"status": "success",
45+
"data": [
46+
{
47+
"templateId": "11111111-1111-1111-1111-111111111111",
48+
"templateName": "Work",
49+
"createdAt": "2026-05-14T02:10:00Z",
50+
"updatedAt": "2026-05-14T02:10:00Z",
51+
"deletedAt": null,
52+
"preparations": []
53+
}
54+
]
55+
}
56+
```
57+
58+
### Get Template Detail
59+
`GET /preparation-templates/{templateId}`
60+
61+
Works for active and soft-deleted templates owned by the user. Detail includes `deletedAt`.
62+
63+
### Create Template
64+
`POST /preparation-templates`
65+
66+
```json
67+
{
68+
"templateId": "11111111-1111-1111-1111-111111111111",
69+
"templateName": "Work",
70+
"preparations": [
71+
{
72+
"preparationId": "22222222-2222-2222-2222-222222222222",
73+
"preparationName": "Pack laptop",
74+
"preparationTime": 5,
75+
"orderIndex": 0
76+
}
77+
]
78+
}
79+
```
80+
81+
Active template names are unique per user after trimming and case-insensitive normalization. Deleted template names can be reused, but deleted template IDs cannot.
82+
83+
### Update Template
84+
`PUT /preparation-templates/{templateId}`
85+
86+
Full replace of name and steps. Deleted templates cannot be updated.
87+
88+
### Delete Template
89+
`DELETE /preparation-templates/{templateId}`
90+
91+
Soft-deletes the template. Existing schedules that already use the template keep using it. New schedule create/update cannot select it. Repeated delete succeeds.
92+
93+
## Schedule Create
94+
`POST /schedules`
95+
96+
Preparation source is inferred:
97+
98+
- Omit both `preparationTemplateId` and `customPreparations`: creates `DEFAULT`.
99+
- Send only `preparationTemplateId`: creates `TEMPLATE`.
100+
- Send only `customPreparations`: creates `CUSTOM`.
101+
- Send both: rejected.
102+
103+
Template mode:
104+
105+
```json
106+
{
107+
"scheduleId": "33333333-3333-3333-3333-333333333333",
108+
"placeId": "44444444-4444-4444-4444-444444444444",
109+
"placeName": "Office",
110+
"scheduleName": "Morning meeting",
111+
"moveTime": 20,
112+
"scheduleTime": "2026-06-01T09:30:00",
113+
"scheduleSpareTime": 10,
114+
"scheduleNote": "Bring laptop",
115+
"preparationTemplateId": "11111111-1111-1111-1111-111111111111"
116+
}
117+
```
118+
119+
Custom mode:
120+
121+
```json
122+
{
123+
"scheduleId": "33333333-3333-3333-3333-333333333333",
124+
"placeId": "44444444-4444-4444-4444-444444444444",
125+
"placeName": "Office",
126+
"scheduleName": "Morning meeting",
127+
"moveTime": 20,
128+
"scheduleTime": "2026-06-01T09:30:00",
129+
"customPreparations": [
130+
{
131+
"preparationId": "55555555-5555-5555-5555-555555555555",
132+
"preparationName": "Pack laptop",
133+
"preparationTime": 5,
134+
"orderIndex": 0
135+
}
136+
]
137+
}
138+
```
139+
140+
## Schedule Update
141+
`PUT /schedules/{scheduleId}`
142+
143+
If `preparationMode` is omitted, the current preparation source is preserved. This includes schedules linked to soft-deleted templates.
144+
145+
To change source:
146+
147+
```json
148+
{
149+
"placeId": "44444444-4444-4444-4444-444444444444",
150+
"placeName": "Office",
151+
"scheduleName": "Morning meeting",
152+
"moveTime": 20,
153+
"scheduleTime": "2026-06-01T09:30:00",
154+
"preparationMode": "DEFAULT"
155+
}
156+
```
157+
158+
```json
159+
{
160+
"placeId": "44444444-4444-4444-4444-444444444444",
161+
"placeName": "Office",
162+
"scheduleName": "Morning meeting",
163+
"moveTime": 20,
164+
"scheduleTime": "2026-06-01T09:30:00",
165+
"preparationMode": "TEMPLATE",
166+
"preparationTemplateId": "11111111-1111-1111-1111-111111111111"
167+
}
168+
```
169+
170+
```json
171+
{
172+
"placeId": "44444444-4444-4444-4444-444444444444",
173+
"placeName": "Office",
174+
"scheduleName": "Morning meeting",
175+
"moveTime": 20,
176+
"scheduleTime": "2026-06-01T09:30:00",
177+
"preparationMode": "CUSTOM",
178+
"customPreparations": [
179+
{
180+
"preparationId": "55555555-5555-5555-5555-555555555555",
181+
"preparationName": "Pack laptop",
182+
"preparationTime": 5,
183+
"orderIndex": 0
184+
}
185+
]
186+
}
187+
```
188+
189+
Mixed mode payloads are rejected:
190+
- `DEFAULT` with template ID or custom list.
191+
- `TEMPLATE` without template ID.
192+
- `TEMPLATE` with custom list.
193+
- `CUSTOM` without full custom list.
194+
- `CUSTOM` with template ID.
195+
196+
Started schedules cannot be edited.
197+
198+
## Schedule Responses
199+
Normal schedule list/detail responses include metadata, not full step lists:
200+
201+
```json
202+
{
203+
"scheduleId": "33333333-3333-3333-3333-333333333333",
204+
"scheduleName": "Morning meeting",
205+
"startedAt": null,
206+
"finishedAt": null,
207+
"preparationMode": "TEMPLATE",
208+
"preparationTemplateId": "11111111-1111-1111-1111-111111111111",
209+
"preparationTemplateName": "Work",
210+
"preparationTemplateDeleted": false,
211+
"preparationFrozen": false
212+
}
213+
```
214+
215+
For default and custom schedules, `preparationTemplateId` and `preparationTemplateName` are `null`.
216+
217+
For schedules linked to a deleted template:
218+
219+
```json
220+
{
221+
"preparationMode": "TEMPLATE",
222+
"preparationTemplateId": "11111111-1111-1111-1111-111111111111",
223+
"preparationTemplateName": "Work",
224+
"preparationTemplateDeleted": true
225+
}
226+
```
227+
228+
## Existing Compatibility Endpoints
229+
These remain linked-list shaped:
230+
231+
- `GET /users/preparations`
232+
- `PUT /users/preparations`
233+
- `GET /schedules/{scheduleId}/preparations`
234+
- `POST /schedules/{scheduleId}/preparations`
235+
- `PUT /schedules/{scheduleId}/preparations`
236+
237+
`POST/PUT /schedules/{scheduleId}/preparations` now means "make this schedule CUSTOM" and clears any template link. The request still uses `nextPreparationId`.
238+
239+
## Alarm Window
240+
`GET /schedules/alarm-window` continues to include full `preparations`, and now also includes the same preparation metadata fields as normal schedule responses.
241+
242+
## Error Codes To Handle
243+
- `PREPARATION_TEMPLATE_NOT_FOUND`: missing or cross-user template.
244+
- `PREPARATION_TEMPLATE_NAME_DUPLICATE`: active template name already exists.
245+
- `PREPARATION_TEMPLATE_LIMIT_EXCEEDED`: user already has 20 active named templates.
246+
- `PREPARATION_TEMPLATE_DELETED`: user tried to select or update an owned deleted template.
247+
- `PREPARATION_STEP_ID_CONFLICT`: provided step ID belongs to another preparation resource.
248+
- `INVALID_INPUT`: malformed mode combination, ordering, list size, duration, or linked-list payload.

0 commit comments

Comments
 (0)