main
HerrHase 3 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 # dotenv environment variable files
.env .env
.env.test
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local

@ -1,3 +1,31 @@
# Nano Session # Nano Session
Functions and Classes for handle session with [@nano/sqlite](https://git.node001.net/nano/sqlite) in [Bun](https://bun.sh/). 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 Session from './src/session.ts'
import SessionStore from './src/sessionStore.ts' import SessionStore from './src/sessionStore.ts'
export default { export {
'Session': Session, Session,
'SessionStore': SessionStore SessionStore
} }

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

@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
user_id INTEGER,
user TEXT, user TEXT,
token TEXT, token TEXT,
expired_at 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 dayjs from 'dayjs'
import SessionStore from './sessionStore.ts' import SessionStore from './sessionStore.ts'
@ -6,23 +6,47 @@ import SessionStore from './sessionStore.ts'
/** /**
* Session * 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 { class Session {
/** // database object
*
*
*/
private db 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) { constructor(db: object) {
this.db = db this.db = db
this.sessionStore = new SessionStore(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 * @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) const users = await this.sessionStore.findOneByUser(userHmac)
// if users are not longer can login, drop oldest // 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) const session = await this.sessionStore.dropOldest(userHash)
} }
@ -52,15 +77,16 @@ class Session {
const date = dayjs() const date = dayjs()
await this.sessionStore.create({ await this.sessionStore.create({
'user_id': userId,
'user' : userHmac, 'user' : userHmac,
'token' : tokenHmac, '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() 'created_at': date.toISOString()
}) })
return { return {
'user': userHmac, 'user': userHash,
'token': token // raw token goes in cookie 'token': token // raw token goes in the cookie
} }
} }
@ -70,8 +96,8 @@ class Session {
* @param object userId * @param object userId
* *
*/ */
public async destroy(user: object): void { public async destroy(sessionId: number): void {
await this.sessionStore.remove(user.id) await this.sessionStore.remove(sessionId)
} }
/** /**
@ -81,11 +107,11 @@ class Session {
* *
*/ */
public async destroyAll(userId: number): void { public async destroyAll(userId: number): void {
const userHash = this.createUserHmac(userId) const userHmac = this.createUserHmac(this.createUserHash(userId))
const users = await this.sessionStore.findByUser(userHash) const sessions = await this.sessionStore.findByUser(userHmac)
for (const index in users) { for (const index in sessions) {
await this.destroy(users[index]) await this.destroy(sessions[index].id)
} }
} }
@ -100,21 +126,21 @@ class Session {
* @return string * @return string
* *
*/ */
public async get(userId: number, agent: string, language: string, ip: string, token: string): string { public async get(userHash: string, token: string, agent: string, language: string, ip = ''): string {
const userHash = this.createUserHmac(userId) const userHmac = this.createUserHmac(userHash)
const tokenHash = this.createTokenHmac(token, agent, language, ip) 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 found, check expired_at, if expired, delete session
if (user && user.expired_at) { if (session && session.expired_at) {
if (dayjs().isAfter(dayjs(user.expired_at))) { if (dayjs().isAfter(dayjs(session.expired_at))) {
await this.destroy(user) await this.destroy(session.id)
user = null session = null
} }
} }
return user return session
} }
/** /**
@ -124,10 +150,24 @@ class Session {
* @return string * @return string
* *
*/ */
private createUserHmac(userId: number): string { private createUserHash(userId: number): string {
const hmac = createHmac('sha512', process.env.NANO_SESSION_SECRET) const hash = createHash('sha256')
const buffer = new Buffer(userId) 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') return hmac.update(buffer).digest('base64')
} }
@ -141,7 +181,7 @@ class Session {
* @return string * @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 hmac = createHmac('sha512', process.env.NANO_SESSION_SECRET)
const buffer = new Buffer(token + agent + language + ip) const buffer = new Buffer(token + agent + language + ip)

@ -1,8 +1,11 @@
import { Store } from '@nano/sqlite' 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 { class SessionStore extends Store {
@ -11,25 +14,25 @@ class SessionStore extends Store {
super(db, 'sessions') super(db, 'sessions')
} }
public async findOneByUser(user) { public async findOneByUser(userHmac) {
return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user') return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user')
.get({ .get({
'$user': user '$user': userHmac
}) })
} }
public async findByUser(user) { public async findByUser(userHmac) {
return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user') return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user')
.all({ .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') return this.db.query('SELECT * FROM ' + this.tableName + ' WHERE user = $user AND token = $token')
.get({ .get({
'$user' : user, '$user' : userHmac,
'$token' : token '$token' : tokenHmac
}) })
} }
} }

@ -7,6 +7,15 @@ import path from 'path'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import Session from './../src/session.ts' 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 () => { test('session / create', async () => {
const db = getOrCreateSqlite({ 'name': ':memory:', 'create': true, 'readwrite': true }) const db = getOrCreateSqlite({ 'name': ':memory:', 'create': true, 'readwrite': true })
await runMigrationSqlite(path.resolve(__dirname, './../src/migration'), db) await runMigrationSqlite(path.resolve(__dirname, './../src/migration'), db)
@ -14,9 +23,9 @@ test('session / create', async () => {
dotenv.config('./../.env.test') dotenv.config('./../.env.test')
const session = new Session(db) 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 () => { test('session / find', async () => {
@ -26,11 +35,25 @@ test('session / find', async () => {
dotenv.config('./../.env.test') dotenv.config('./../.env.test')
const session = new Session(db) 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 () => { test('session / expired', async () => {
@ -40,14 +63,14 @@ test('session / expired', async () => {
dotenv.config('./../.env.test') dotenv.config('./../.env.test')
const session = new Session(db) 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 // change manually session, and set expired_at to
const sessionStore = new SessionStore(db) 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 // 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 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) 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, request.agent, request.language, request.ip)
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, request.agent, request.language, request.ip)
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, request.agent, request.language, request.ip)
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, request.agent, request.language, request.ip)
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.destroyAll(1) await session.destroyAll(1)
const result = db.query("SELECT * FROM sessions").all() const result = db.query("SELECT * FROM sessions").all()

Loading…
Cancel
Save