feat(auth): Session model + remember-me
Replace the single `user.refreshToken` column with a proper Session
table so users can have multiple concurrent sessions (phone, laptop,
etc.), each with their own refresh token, expiry, label, and
remember-me flag.
- Add Session model (id, userId, tokenHash, label, userAgent,
ipAddress, rememberMe, lastUsedAt, expiresAt).
- Drop `User.refreshToken` and `User.refreshTokenExpiresAt`.
- authService: new createSession/validateSession/rotateSession/
revokeSession/listUserSessions helpers; remove refresh-token-on-user
functions.
- sessionCookies helper now issues a session_id cookie alongside
access_token and refresh_token; rotateSessionCookies keeps the same
session id on refresh.
- Login form adds a "Keep me signed in for 30 days" checkbox;
TTL is 7d by default, 30d with remember-me.
- User-Agent parsed into a friendly label ("Chrome on Windows") for
the upcoming sessions page.
- hooks.server.ts, refresh endpoint, logout, register, oauth callback,
and onboarding all switched to the new session API.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"label" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"ipAddress" TEXT,
|
||||
"rememberMe" BOOLEAN NOT NULL DEFAULT false,
|
||||
"lastUsedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_expiresAt_idx" ON "Session"("expiresAt");
|
||||
|
||||
-- RedefineTables: drop refreshToken + refreshTokenExpiresAt from User.
|
||||
-- All existing user sessions will be invalidated; users must re-login once after upgrade.
|
||||
PRAGMA foreign_keys=OFF;
|
||||
|
||||
CREATE TABLE "new_User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"email" TEXT NOT NULL,
|
||||
"password" TEXT,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"avatarUrl" TEXT,
|
||||
"authProvider" TEXT NOT NULL DEFAULT 'local',
|
||||
"role" TEXT NOT NULL DEFAULT 'user',
|
||||
"onboardingComplete" BOOLEAN NOT NULL DEFAULT false,
|
||||
"trackRecentApps" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"themeMode" TEXT,
|
||||
"primaryHue" INTEGER,
|
||||
"primarySaturation" INTEGER,
|
||||
"backgroundType" TEXT,
|
||||
"locale" TEXT
|
||||
);
|
||||
|
||||
INSERT INTO "new_User" (
|
||||
"id", "email", "password", "displayName", "avatarUrl", "authProvider", "role",
|
||||
"onboardingComplete", "trackRecentApps", "createdAt", "updatedAt",
|
||||
"themeMode", "primaryHue", "primarySaturation", "backgroundType", "locale"
|
||||
)
|
||||
SELECT
|
||||
"id", "email", "password", "displayName", "avatarUrl", "authProvider", "role",
|
||||
"onboardingComplete", "trackRecentApps", "createdAt", "updatedAt",
|
||||
"themeMode", "primaryHue", "primarySaturation", "backgroundType", "locale"
|
||||
FROM "User";
|
||||
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||
|
||||
PRAGMA foreign_keys=ON;
|
||||
+19
-2
@@ -15,8 +15,6 @@ model User {
|
||||
avatarUrl String?
|
||||
authProvider String @default("local") // local | oauth
|
||||
role String @default("user") // admin | user
|
||||
refreshToken String?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
onboardingComplete Boolean @default(false)
|
||||
trackRecentApps Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -29,6 +27,7 @@ model User {
|
||||
locale String?
|
||||
|
||||
groups UserGroup[]
|
||||
sessions Session[]
|
||||
createdApps App[]
|
||||
boards Board[]
|
||||
favorites UserFavorite[]
|
||||
@@ -42,6 +41,24 @@ model User {
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
tokenHash String // bcrypt hash of the refresh token
|
||||
label String? // user-friendly, e.g. "Chrome on Windows"
|
||||
userAgent String?
|
||||
ipAddress String?
|
||||
rememberMe Boolean @default(false)
|
||||
lastUsedAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
model Group {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
|
||||
Reference in New Issue
Block a user