diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 782380756..e6d8e6d19 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -898,6 +898,14 @@ export default {
await models.user.update({ where: { id: me.id }, data: { hideWelcomeBanner: true } })
return true
+ },
+ invalidateSessions: async (parent, args, { me, models }) => {
+ if (!me) {
+ throw new GqlAuthenticationError()
+ }
+
+ await models.user.update({ where: { id: me.id }, data: { sessionRev: { increment: 1 } } })
+ return true
}
},
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index fd26838ce..0453f808f 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -44,6 +44,7 @@ export default gql`
generateApiKey(id: ID!): String
deleteApiKey(id: ID!): User
disableFreebies: Boolean
+ invalidateSessions: Boolean
}
type User {
@@ -197,6 +198,7 @@ export default gql`
walletsUpdatedAt: Date
proxyReceive: Boolean
directReceive: Boolean
+ sessionRev: Int
receiveCreditsBelowSats: Int!
sendCreditsBelowSats: Int!
}
diff --git a/fragments/users.js b/fragments/users.js
index 94e1a7a67..b56f62100 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -54,6 +54,7 @@ ${STREAK_FIELDS}
walletsUpdatedAt
proxyReceive
directReceive
+ sessionRev
}
optional {
isContributor
@@ -117,6 +118,7 @@ export const SETTINGS_FIELDS = gql`
apiKeyEnabled
proxyReceive
directReceive
+ sessionRev
receiveCreditsBelowSats
sendCreditsBelowSats
}
diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js
index 8f2d7ffa4..431ded963 100644
--- a/pages/api/auth/[...nextauth].js
+++ b/pages/api/auth/[...nextauth].js
@@ -98,6 +98,7 @@ function getCallbacks (req, res) {
// token won't have an id on it for new logins, we add it
// note: token is what's kept in the jwt
token.id = Number(user.id)
+ token.sessionRev = user.sessionRev || 0
// if referrer exists, set on user
// isNewUser doesn't work for nostr/lightning auth because we create the user before nextauth can
@@ -143,6 +144,11 @@ function getCallbacks (req, res) {
// and returns a new object session that's returned whenever get|use[Server]Session is called
session.user.id = token.id
+ // invalidate this session if session revision mismatch
+ session.user.sessionRev = token.sessionRev || 0 // if no sessionRev, set to 0, the user will have one after login
+ const sessionRev = await prisma.user.findUnique({ where: { id: session.user.id }, select: { sessionRev: true } })
+ if (session.user.sessionRev !== sessionRev?.sessionRev) return {}
+
return session
}
}
diff --git a/pages/settings/index.js b/pages/settings/index.js
index 5ad2f813c..c6cc48c9e 100644
--- a/pages/settings/index.js
+++ b/pages/settings/index.js
@@ -886,6 +886,7 @@ function AuthMethods ({ methods, apiKeyEnabled }) {
)
}
})}
+
>
)
@@ -1188,3 +1189,29 @@ const TipRandomField = () => {
>
)
}
+
+const InvalidateSessions = () => {
+ const showModal = useShowModal()
+ const router = useRouter()
+
+ const [invalidateSessions] = useMutation(gql`
+ mutation invalidateSessions {
+ invalidateSessions
+ }`
+ )
+
+ return (
+
+ )
+}
diff --git a/prisma/migrations/20250221103652_invalidate_sessions/migration.sql b/prisma/migrations/20250221103652_invalidate_sessions/migration.sql
new file mode 100644
index 000000000..eea333e55
--- /dev/null
+++ b/prisma/migrations/20250221103652_invalidate_sessions/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "sessionRev" INTEGER NOT NULL DEFAULT 0;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index a3f986b62..4af1d5980 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -123,6 +123,7 @@ model User {
mcredits BigInt @default(0)
receiveCreditsBelowSats Int @default(10)
sendCreditsBelowSats Int @default(10)
+ sessionRev Int @default(0)
muters Mute[] @relation("muter")
muteds Mute[] @relation("muted")
ArcOut Arc[] @relation("fromUser")