@nr1e/lucia-adapter-dynamodb
Version:
A DynamoDB adapter for lucia-auth
176 lines • 6.31 kB
JavaScript
import { BatchWriteItemCommand, DeleteItemCommand, PutItemCommand, QueryCommand, UpdateItemCommand, GetItemCommand, } from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
const MAX_BATCH_SIZE = 25;
/**
* Adapter using two GSIs
*/
export class DynamoDBAdapter {
client;
tableName = 'LuciaAuthTable';
getUser;
consistentRead;
constructor(options) {
this.client = options.client;
if (options.tableName)
this.tableName = options.tableName;
this.getUser = options.getUser;
this.consistentRead = options?.consistentRead ?? false;
}
async deleteSession(sessionId) {
await this.client.send(new DeleteItemCommand({
TableName: this.tableName,
Key: {
sid: { S: sessionId },
},
}));
}
async deleteSessions(commandInput) {
let _lastEvaluatedKey = undefined;
const keys = [];
// query keys
do {
if (_lastEvaluatedKey)
commandInput.ExclusiveStartKey = _lastEvaluatedKey;
const res = await this.client.send(new QueryCommand(commandInput));
if (res?.Items?.length) {
const expiredSessions = res.Items.map((x) => unmarshall(x));
keys.push(...expiredSessions.map((item) => ({
sid: { S: item.sid },
})));
}
_lastEvaluatedKey = res?.LastEvaluatedKey;
// delete all keys
for (let i = 0; i < keys.length; i += MAX_BATCH_SIZE) {
const batch = keys.slice(i, i + MAX_BATCH_SIZE);
await this.client.send(new BatchWriteItemCommand({
RequestItems: {
[this.tableName]: batch.map((key) => ({
DeleteRequest: { Key: key },
})),
},
}));
}
} while (_lastEvaluatedKey);
}
async deleteUserSessions(userId) {
await this.deleteSessions({
TableName: this.tableName,
IndexName: 'Gs1',
KeyConditionExpression: '#uid = :uid',
ExpressionAttributeNames: {
'#uid': 'uid',
'#sid': 'sid',
},
ExpressionAttributeValues: {
':uid': { S: userId },
},
Select: 'SPECIFIC_ATTRIBUTES',
ProjectionExpression: '#sid',
});
}
async getSessionAndUser(sessionId) {
const sessionRes = await this.client.send(new GetItemCommand({
TableName: this.tableName,
Key: {
sid: { S: sessionId },
},
ConsistentRead: this.consistentRead,
}));
if (!sessionRes.Item)
return [null, null];
const session = this.itemToSession(sessionRes.Item);
const user = await this.getUser(session.userId, this.client);
return [session, user];
}
async getUserSessions(userId) {
const sessions = [];
let _lastEvaluatedKey = undefined;
do {
const commandInput = {
TableName: this.tableName,
IndexName: 'Gs1',
ExpressionAttributeNames: {
'#uid': 'uid',
},
ExpressionAttributeValues: {
':uid': { S: userId },
},
KeyConditionExpression: '#uid = :uid',
};
if (_lastEvaluatedKey)
commandInput.ExclusiveStartKey = _lastEvaluatedKey;
const res = await this.client.send(new QueryCommand(commandInput));
if (res?.Items?.length) {
sessions.push(...res.Items.map((x) => this.itemToSession(x)));
}
_lastEvaluatedKey = res?.LastEvaluatedKey;
} while (_lastEvaluatedKey);
return sessions;
}
async setSession(databaseSession) {
const expires = Math.floor(databaseSession.expiresAt.getTime() / 1000);
await this.client.send(new PutItemCommand({
TableName: this.tableName,
Item: marshall({
sid: databaseSession.id,
uid: databaseSession.userId,
exp: expires,
typ: 'Session',
attrs: databaseSession.attributes,
}),
}));
}
async updateSessionExpiration(sessionId, expiresAt) {
if (expiresAt.getTime() > Date.now()) {
const expires = Math.floor(expiresAt.getTime() / 1000);
// update the session
await this.client.send(new UpdateItemCommand({
TableName: this.tableName,
Key: {
sid: { S: sessionId },
},
UpdateExpression: 'SET #exp = :exp',
ConditionExpression: '#sid = :sid',
ExpressionAttributeNames: {
'#sid': 'sid',
'#exp': 'exp',
},
ExpressionAttributeValues: {
':exp': { N: `${expires}` },
':sid': { S: sessionId },
},
}));
}
else {
await this.deleteSession(sessionId);
}
}
async deleteExpiredSessions() {
await this.deleteSessions({
TableName: this.tableName,
IndexName: 'Gs2',
ExpressionAttributeNames: {
'#sid': 'sid',
'#exp': 'exp',
'#typ': 'typ',
},
ExpressionAttributeValues: {
':typ': { S: 'Session' },
':exp': { N: `${Math.floor(new Date().getTime() / 1000)}` },
},
KeyConditionExpression: '#typ = :typ AND #exp < :exp',
Select: 'SPECIFIC_ATTRIBUTES',
ProjectionExpression: '#sid',
});
}
itemToSession(item) {
const unmarshalled = unmarshall(item);
return {
id: unmarshalled.sid,
userId: unmarshalled.uid,
expiresAt: new Date(unmarshalled.exp * 1000),
attributes: unmarshalled.attrs,
};
}
}
//# sourceMappingURL=index.js.map