main
HerrHase 4 months ago
parent bf1c7687c3
commit adc4520d23

@ -0,0 +1 @@
NANO_SESSION_SECRET="your-secret"

1
.gitignore vendored

@ -75,7 +75,6 @@ web_modules/
# dotenv environment variable files
.env
.env.test
.env.development.local
.env.test.local
.env.production.local

@ -1,3 +1,31 @@
# Nano Session
Functions and Classes for handle session with [@nano/sqlite](https://git.node001.net/nano/sqlite) in [Bun](https://bun.sh/).
## API
### Session
```
import { getOrCreateSqlite } from '@nano/sqlite'
const db = getOrCreateSqlite({ 'name': ':memory:', 'create': true, 'readwrite': true })
const session = new Session(db)
```
#### create(userId: number, agent: string, language: string, ip = '')
Create a new Session for User. Agent and Language from User will be used to create a Hmac value in the DB. The IP is optional. If
one of these values are changed, the session is invalid. After Creating the session you get a Random Token, and the userId as
hash. UserHash and Token are
### get(userHash: string, token: string, agent: string, language: string, ip = '')
Get Session for User. If session has expired, the entry will be removed from Database.
### destroy(sessionId: number)
Remove single Session for User with id of Session.
### destroyAll(userId: number)
Remove all session for a User with UserId.

Binary file not shown.

@ -1,7 +1,7 @@
import Session from './src/session.ts'
import SessionStore from './src/sessionStore.ts'
export default {
'Session': Session,
'SessionStore': SessionStore
export {
Session,
SessionStore
}

@ -13,7 +13,7 @@
"test": "bun test ./test/*"
},
"dependencies": {
"@nano/sqlite": "^0.1.0",
"@nano/sqlite": "0.2.0",
"dayjs": "^1.11.12"
},
"devDependencies": {

@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY,
user_id INTEGER,
user TEXT,
token TEXT,
expired_at TEXT,

@ -1,4 +1,4 @@
import { createHmac, randomBytes } from 'node:crypto'
import { createHmac, createHash, randomBytes } from 'node:crypto'
import dayjs from 'dayjs'
import SessionStore from './sessionStore.ts'
@ -6,23 +6,47 @@ import SessionStore from './sessionStore.ts'
/**
* Session
*
* @author Björn Hase <me@herr-hase.wtf>
* @license http://opensource.org/licenses/MIT The MIT License
* @link https://git.node001.net/nano/session.git
*
*/
class Session {
/**
*
*
*/
// database object
private db
// store for sessions
private sessionStore
// max logins for one user
private maxLogins = 5;
// duration for expired_at
private duration = 60;
/**
*
* @param object db
*
*/
constructor(db: object) {
this.db = db
this.sessionStore = new SessionStore(db)
if (!process.env.NANO_SESSION_SECRET) {
throw new Error('NANO_SESSION_SECRET is undefined, please check .env-file')
}
// if NANO_MAX_LOGINS is set in .env
if (process.env.NANO_SESSION_MAX_LOGINS) {
this.maxLogins = process.env.NANO_SESSION_MAX_LOGINS
}
// if NANO_SESSION_DURATION is set in .env
if (process.env.NANO_SESSION_DURATION) {
this.duration = process.env.NANO_SESSION_DURATION
}
}
/**
@ -35,13 +59,14 @@ class Session {
* @return object
*
*/
public async create(userId: number, agent: string, language: string, ip: string): object {
public async create(userId: number, agent: string, language: string, ip = ''): object {
const userHmac = this.createUserHmac(userId)
const userHash = this.createUserHash(userId)
const userHmac = this.createUserHmac(userHash)
const users = await this.sessionStore.findOneByUser(userHmac)
// if users are not longer can login, drop oldest
if (users && users.length > process.env.NANO_MAX_LOGINS) {
if (users && users.length > this.maxLogins) {
const session = await this.sessionStore.dropOldest(userHash)
}
@ -52,15 +77,16 @@ class Session {
const date = dayjs()
await this.sessionStore.create({
'user_id': userId,
'user' : userHmac,
'token' : tokenHmac,
'expired_at': date.add(process.env.NANO_SESSION_DURATION, 'minute').toISOString(),
'expired_at': date.add(this.duration, 'minute').toISOString(),
'created_at': date.toISOString()
})
return {
'user': userHmac,
'token': token // raw token goes in cookie
'user': userHash,
'token': token // raw token goes in the cookie
}
}
@ -70,8 +96,8 @@ class Session {
* @param object userId
*
*/
public async destroy(user: object): void {
await this.sessionStore.remove(user.id)
public async destroy(sessionId: number): void {
await this.sessionStore.remove(sessionId)
}
/**
@ -81,11 +107,11 @@ class Session {
*
*/
public async destroyAll(userId: number): void {
const userHash = this.createUserHmac(userId)
const users = await this.sessionStore.findByUser(userHash)
const userHmac = this.createUserHmac(this.createUserHash(userId))
const sessions = await this.sessionStore.findByUser(userHmac)
for (const index in users) {
await this.destroy(users[index])
for (const index in sessions) {
await this.destroy(sessions[index].id)
}
}
@ -100,21 +126,21 @@ class Session {
* @return string
*
*/
public async get(userId: number, agent: string, language: string, ip: string, token: string): string {
const userHash = this.createUserHmac(userId)
const tokenHash = this.createTokenHmac(token, agent, language, ip)
public async get(userHash: string, token: string, agent: string, language: string, ip = ''): string {
const userHmac = this.createUserHmac(userHash)
const tokenHmac = this.createTokenHmac(token, agent, language, ip)
let user = await this.sessionStore.findOneByUserAndToken(userHash, tokenHash)
let session = await this.sessionStore.findOneByUserAndToken(userHmac, tokenHmac)
// if user found, check expired_at, if expired, delete session
if (user && user.expired_at) {
if (dayjs().isAfter(dayjs(user.expired_at))) {
await this.destroy(user)
user = null
if (session && session.expired_at) {
if (dayjs().isAfter(dayjs(session.expired_at))) {
await this.destroy(session.id)
session = null
}
}
return user
return session
}
/**
@ -124,10 +150,24 @@ class Session {
* @return string
*
*/
private createUserHmac(userId: number): string {
const hmac = createHmac('sha512', process.env.NANO_SESSION_SECRET)
private createUserHash(userId: number): string {
const hash = createHash('sha256')
const buffer = new Buffer(userId)
return hash.update(buffer).digest('hex')
}
/**
* creating hmac for userId
*
* @param number userId
* @return string
*
*/
private createUserHmac(userHash: string): string {
const hmac = createHmac('sha512', process.env.NANO_SESSION_SECRET)
const buffer = new Buffer(userHash)
return hmac.update(buffer).digest('base64')
}
@ -141,7 +181,7 @@ class Session {
* @return string
*
*/
private createTokenHmac(token: string, agent: string, language: string, ip: string): string {
private createTokenHmac(token: string, agent: string, language: string, ip = ''): string {
const hmac = createHmac('sha512', process.env.NANO_SESSION_SECRET)
const buffer = new Buffer(token + agent + language + ip)

@ -1,8 +1,11 @@
import { Store } from '@nano/sqlite'
/**
* Session
* SessionStore
*
* @author Björn Hase <me@herr-hase.wtf>
* @license http://opensource.org/licenses/MIT The MIT License
* @link https://git.node001.net/nano/session.git
*
*/
class SessionStore extends Store {
@ -11,25 +14,25 @@ class SessionStore extends Store {
super(db, 'sessions')
}
public async findOneByUser(user) {
public async findOneByUser(userHmac) {
return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user')
.get({
'$user': user
'$user': userHmac
})
}
public async findByUser(user) {
public async findByUser(userHmac) {
return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user')
.all({
'$user': user
'$user': userHmac
})
}
public async findOneByUserAndToken(user, token) {
public async findOneByUserAndToken(userHmac, tokenHmac) {
return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user AND token = $token')
.get({
'$user' : user,
'$token' : token
'$user' : userHmac,
'$token' : tokenHmac
})
}
}

@ -7,6 +7,15 @@ import path from 'path'
import dotenv from 'dotenv'
import Session from './../src/session.ts'
const userHashResult = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d'
const userHmacResult = 'qMMo5J3Kq0q3+IPadNretbmitllWakjUTD/6HnTRdSNoifrjkLEf5p6sc0qx2q0bSGTeqXI4OaP5VuUQk0EzuQ=='
const request = {
agent: 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0',
agentChrome: 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Chrome/121.0',
language: 'de,en-US;q=0.8,en;q=0.10',
ip: '1.1.1.1'
}
test('session / create', async () => {
const db = getOrCreateSqlite({ 'name': ':memory:', 'create': true, 'readwrite': true })
await runMigrationSqlite(path.resolve(__dirname, './../src/migration'), db)
@ -14,9 +23,9 @@ test('session / create', async () => {
dotenv.config('./../.env.test')
const session = new Session(db)
const result = await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.1')
const result = await session.create(1, request.agent, request.language, request.ip)
expect('3qC9SK5Z8XC0DXQXs3Fs5CbK5ZS9YOVZziNT8Ulzc2dehXh9qJNMyoVl6jGJJnbbd77/qPdID2wRDFEzqtKshg==').toEqual(result.user)
expect(result.user).toEqual(userHashResult)
})
test('session / find', async () => {
@ -26,11 +35,25 @@ test('session / find', async () => {
dotenv.config('./../.env.test')
const session = new Session(db)
const result = await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.1')
const result = await session.create(1, request.agent, request.language, request.ip)
const userSession = await session.get(result.user, result.token, request.agent, request.language, request.ip)
expect(userHmacResult).toEqual(userSession.user)
})
test('session / find / change browser', async () => {
const db = getOrCreateSqlite({ 'name': ':memory:', 'create': true, 'readwrite': true })
await runMigrationSqlite(path.resolve(__dirname, './../src/migration'), db)
dotenv.config('./../.env.test')
const session = new Session(db)
const result = await session.create(1, request.agent, request.language, request.ip)
const user = await session.get(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.1', result.token)
const userSession = await session.get(result.user, result.token, request.agentChrome, request.language, request.ip)
expect('3qC9SK5Z8XC0DXQXs3Fs5CbK5ZS9YOVZziNT8Ulzc2dehXh9qJNMyoVl6jGJJnbbd77/qPdID2wRDFEzqtKshg==').toEqual(user.user)
expect(null).toEqual(userSession)
})
test('session / expired', async () => {
@ -40,14 +63,14 @@ test('session / expired', async () => {
dotenv.config('./../.env.test')
const session = new Session(db)
const result = await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.1')
const result = await session.create(1, request.agent, request.language, request.ip)
// change manually session, and set expired_at to
const sessionStore = new SessionStore(db)
await sessionStore.update(1, { 'expired_at': dayjs().toISOString() })
await sessionStore.update(1, { 'expired_at': dayjs().subtract(60, 'minute').toISOString() })
// getting user
const user = await session.get(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.1', result.token)
const user = await session.get(result.user, result.token, request.agent, request.language, request.ip)
expect(null).toEqual(user) // user not found, because expired user is deleted
})
@ -60,11 +83,11 @@ test('session / destroy all', async () => {
const session = new Session(db)
await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.1')
await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.2')
await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.3')
await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.4')
await session.create(1, 'Mozilla/2.0 (X11; Windows x86;) Gecko/20100101 Firefox/120.0', 'de,en-US;q=0.7,en;q=0.3', '1.1.1.5')
await session.create(1, request.agent, request.language, request.ip)
await session.create(1, request.agent, request.language, request.ip)
await session.create(1, request.agent, request.language, request.ip)
await session.create(1, request.agent, request.language, request.ip)
await session.create(1, request.agent, request.language, request.ip)
await session.destroyAll(1)
const result = db.query("SELECT * FROM sessions").all()

Loading…
Cancel
Save