Skip to content

Commit 3b24875

Browse files
authored
Merge pull request #245 from itsmeakhil/feat/tauri-desktop-v2
Feat/tauri desktop v2
2 parents 5c0a37e + 8d28914 commit 3b24875

161 files changed

Lines changed: 15530 additions & 565 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.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,6 @@ apps/desktop/src-tauri/target/release/
7474
.claude
7575
/docs
7676

77-
.superpowers
77+
.superpowers/apps/desktop/node_modules
78+
/apps/desktop/src-tauri/target
79+
/apps/desktop/node_modules

apps/api-runner/node_modules/.bin/tsc

Lines changed: 3 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/api-runner/node_modules/.bin/tsserver

Lines changed: 3 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/backend/app/api/routes/auth/api.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from app.core.limiter import limiter
1010
from app.core import audit
11+
from app.core.config import get_settings
1112

1213
from app.api.routes.auth.cookie_attach import attach_auth_cookies, clear_auth_cookies
1314
from app.api.routes.auth.schema import (
@@ -80,8 +81,16 @@ async def create_session(
8081

8182
access = create_access_token(uid)
8283
raw_refresh = new_refresh_token()
83-
await set_refresh_token_hash(uid, hash_refresh_token(raw_refresh))
84-
attach_auth_cookies(response, access, raw_refresh)
84+
settings = get_settings()
85+
refresh_days = (
86+
settings.LONG_LIVED_REFRESH_TOKEN_EXPIRE_DAYS
87+
if payload.long_lived
88+
else settings.REFRESH_TOKEN_EXPIRE_DAYS
89+
)
90+
await set_refresh_token_hash(
91+
uid, hash_refresh_token(raw_refresh), long_lived=payload.long_lived
92+
)
93+
attach_auth_cookies(response, access, raw_refresh, refresh_days=refresh_days)
8594

8695
doc = await get_user_doc(uid)
8796
if not doc:
@@ -132,10 +141,20 @@ async def refresh_session(
132141
detail="Invalid refresh token.",
133142
)
134143

144+
# Preserve the session's long-lived flag across rotation so desktop
145+
# sessions keep their 60-day cookie TTL on every refresh.
146+
user_doc = await get_user_doc(uid)
147+
long_lived = bool(user_doc.get("refresh_long_lived", False)) if user_doc else False
148+
settings = get_settings()
149+
refresh_days = (
150+
settings.LONG_LIVED_REFRESH_TOKEN_EXPIRE_DAYS
151+
if long_lived
152+
else settings.REFRESH_TOKEN_EXPIRE_DAYS
153+
)
135154
new_raw = new_refresh_token()
136-
await set_refresh_token_hash(uid, hash_refresh_token(new_raw))
155+
await set_refresh_token_hash(uid, hash_refresh_token(new_raw), long_lived=long_lived)
137156
access = create_access_token(uid)
138-
attach_auth_cookies(response, access, new_raw)
157+
attach_auth_cookies(response, access, new_raw, refresh_days=refresh_days)
139158
audit.set_action("auth.token_refresh")
140159
audit.set_entity("user", uid)
141160
audit.set_summary("Refreshed session")
@@ -300,6 +319,26 @@ async def session_check(_uid: Annotated[str, Depends(get_current_uid)]) -> OkRes
300319
return OkResponse(ok=True)
301320

302321

322+
@router.post(
323+
"/desktop-token",
324+
summary="Mint a Firebase custom token for the desktop-app sign-in handoff",
325+
)
326+
async def desktop_token(uid: Annotated[str, Depends(get_current_uid)]) -> dict:
327+
# The web session (browser) mints a short-lived Firebase custom token which
328+
# is handed to the desktop app via the mydevtools:// deep link; the app
329+
# signs in with it and runs the normal session exchange.
330+
from app.core.firebase import get_firebase_app
331+
332+
try:
333+
from firebase_admin import auth as firebase_auth
334+
except Exception: # pragma: no cover - firebase_admin always present in prod
335+
raise HTTPException(status_code=503, detail="Firebase unavailable")
336+
get_firebase_app()
337+
token_bytes = firebase_auth.create_custom_token(uid)
338+
token = token_bytes.decode("utf-8") if isinstance(token_bytes, bytes) else token_bytes
339+
return {"token": token}
340+
341+
303342
# ── Master-password vault ─────────────────────────────────────────────────────
304343

305344

apps/backend/app/api/routes/auth/cookie_attach.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44
from app.core.config import get_settings
55

66

7-
def attach_auth_cookies(response: Response, access: str, refresh_plain: str) -> None:
7+
def attach_auth_cookies(
8+
response: Response,
9+
access: str,
10+
refresh_plain: str,
11+
refresh_days: int | None = None,
12+
) -> None:
813
settings = get_settings()
914
access_max = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
10-
refresh_max = settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 3600
15+
days = refresh_days if refresh_days is not None else settings.REFRESH_TOKEN_EXPIRE_DAYS
16+
refresh_max = days * 24 * 3600
1117
common: dict = {
1218
"httponly": True,
1319
"secure": settings.AUTH_COOKIE_SECURE,

apps/backend/app/api/routes/auth/schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class SessionRequest(BaseModel):
77

88
id_token: str = Field(min_length=1)
99
check_revoked: bool = True
10+
# Desktop app requests a long-lived (60-day) refresh cookie.
11+
long_lived: bool = False
1012

1113

1214
class SocialLinks(BaseModel):

apps/backend/app/api/routes/auth/users_repo.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,20 @@ async def update_user_profile(uid: str, updates: dict[str, Any]) -> None:
8383
await bump_version(ns="auth_user", uid=uid)
8484

8585

86-
async def set_refresh_token_hash(uid: str, token_hash: str) -> None:
86+
async def set_refresh_token_hash(
87+
uid: str, token_hash: str, long_lived: bool = False
88+
) -> None:
8789
now = create_timestamp()
8890
await db_manager.update_one(
8991
USERS,
9092
{"_id": uid},
91-
{"$set": {"refresh_token_hash": token_hash, "updated_at": now}},
93+
{
94+
"$set": {
95+
"refresh_token_hash": token_hash,
96+
"refresh_long_lived": long_lived,
97+
"updated_at": now,
98+
}
99+
},
92100
)
93101
await bump_version(ns="auth_user", uid=uid)
94102

apps/backend/app/api/routes/nosql/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ class ConnectionOut(BaseModel):
3737
name: str
3838
createdAt: int
3939
lastUsedAt: int
40+
updatedAt: int

apps/backend/app/api/routes/nosql/services.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> ConnectionOut:
1818
created_at = int(doc.get("createdAt", 0)) or create_timestamp()
1919
last_used_at = int(doc.get("lastUsedAt", 0)) or created_at
20+
# Content-edit clock for sync LWW. Falls back to lastUsedAt for legacy docs
21+
# written before updatedAt existed.
22+
updated_at = int(doc.get("updatedAt", 0)) or last_used_at
2023
return ConnectionOut(
2124
id=connection_id,
2225
userId=str(doc.get("created_by", "")),
@@ -25,6 +28,7 @@ def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> ConnectionOut:
2528
name=str(doc.get("name", "")),
2629
createdAt=created_at,
2730
lastUsedAt=last_used_at,
31+
updatedAt=updated_at,
2832
)
2933

3034

@@ -52,6 +56,7 @@ async def create_connection(ctx: WorkspaceContext, body: ConnectionCreate) -> Co
5256
"name": body.name or "My Connection",
5357
"createdAt": ts,
5458
"lastUsedAt": ts,
59+
"updatedAt": ts,
5560
}
5661
try:
5762
await db_manager.insert_one(NOSQL_CONNECTIONS, doc)
@@ -69,7 +74,9 @@ async def update_connection(ctx: WorkspaceContext, connection_id: str, body: Con
6974
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found.")
7075

7176
ts = create_timestamp()
72-
patch: dict[str, Any] = {"lastUsedAt": ts}
77+
# updatedAt is the sync LWW clock — it advances on every content edit (unlike
78+
# lastUsedAt, which also moves on a bare connect/touch).
79+
patch: dict[str, Any] = {"lastUsedAt": ts, "updatedAt": ts}
7380
if body.encryptedData is not None:
7481
patch["encryptedData"] = body.encryptedData
7582
if body.iv is not None:

apps/backend/app/api/routes/redis_commander/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ class RedisConnectionOut(BaseModel):
3939
name: str
4040
createdAt: int
4141
lastUsedAt: int
42+
updatedAt: int

0 commit comments

Comments
 (0)