More progress on backend user
This commit is contained in:
Generated
+16
@@ -35,6 +35,7 @@
|
||||
"i18next": "^25.7.2",
|
||||
"iso-3166": "^4.4.0",
|
||||
"iso-639-1": "^3.1.5",
|
||||
"libsodium-wrappers-sumo": "^0.8.2",
|
||||
"material-dynamic-colors": "^1.1.1",
|
||||
"media-captions": "^1.0.4",
|
||||
"melt": "^0.44.0",
|
||||
@@ -10217,6 +10218,21 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libsodium-sumo": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.2.tgz",
|
||||
"integrity": "sha512-uMgnjphJ717jLN+jFG1HUgNrK/gOVVfaO1DGZ1Ig/fKLKLVhvaH/sM1I1v784JFvmkJDaczDpi7xSYC4Jvdo1Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/libsodium-wrappers-sumo": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.2.tgz",
|
||||
"integrity": "sha512-wd1xAY++Kr6VMikSaa4EPRAHJmFvNlGWiiwU3Jh3GR1zRYF3/I3vy/wYsr4k3LVsNzwb9sqfEQ4LdVQ6zEebyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"libsodium-sumo": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"i18next": "^25.7.2",
|
||||
"iso-3166": "^4.4.0",
|
||||
"iso-639-1": "^3.1.5",
|
||||
"libsodium-wrappers-sumo": "^0.8.2",
|
||||
"material-dynamic-colors": "^1.1.1",
|
||||
"media-captions": "^1.0.4",
|
||||
"melt": "^0.44.0",
|
||||
|
||||
@@ -2,6 +2,7 @@ export type IsOwnBackend = {
|
||||
builtWithBackend: boolean;
|
||||
internalAuth: boolean;
|
||||
requireAuth: boolean;
|
||||
registrationAllowed: boolean;
|
||||
};
|
||||
|
||||
export function isOwnBackend(): IsOwnBackend | null {
|
||||
@@ -10,6 +11,7 @@ export function isOwnBackend(): IsOwnBackend | null {
|
||||
return {
|
||||
builtWithBackend: true,
|
||||
internalAuth: import.meta.env.VITE_INTERNAL_AUTH !== 'false',
|
||||
requireAuth: import.meta.env.VITE_REQUIRE_AUTH !== 'false'
|
||||
requireAuth: import.meta.env.VITE_REQUIRE_AUTH !== 'false',
|
||||
registrationAllowed: import.meta.env.VITE_REGISTRATION_ALLOWED === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Sequelize, DataTypes } from 'sequelize';
|
||||
import { Sequelize, DataTypes, type Model } from 'sequelize';
|
||||
import { DATABASE_CONNECTION_URI } from '$env/static/private';
|
||||
|
||||
export const sequelize = new Sequelize(
|
||||
@@ -6,6 +6,14 @@ export const sequelize = new Sequelize(
|
||||
DATABASE_CONNECTION_URI ? DATABASE_CONNECTION_URI : 'sqlite::memory:'
|
||||
);
|
||||
|
||||
export interface UserTableModel extends Model {
|
||||
id: string;
|
||||
username: string;
|
||||
passwordHash: string;
|
||||
passwordSalt: string;
|
||||
created: Date;
|
||||
}
|
||||
|
||||
export const UserTable = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.UUIDV4,
|
||||
@@ -14,24 +22,46 @@ export const UserTable = sequelize.define('User', {
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
passwordHash: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
create: {
|
||||
passwordSalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
created: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
export interface ChannelSubscriptionModel {
|
||||
channelIdCipher: string;
|
||||
channelIdSalt: string;
|
||||
channelNameCipher: string;
|
||||
channelNameSalt: string;
|
||||
lastRSSFetch: Date;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const ChannelSubscriptionTable = sequelize.define('Subscriptions', {
|
||||
channelId: {
|
||||
channelIdCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
channelName: {
|
||||
channelIdSalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
channelNameCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
channelNameSalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
@@ -44,6 +74,7 @@ export const ChannelSubscriptionTable = sequelize.define('Subscriptions', {
|
||||
references: {
|
||||
model: 'User',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { UserTable, type ChannelSubscriptionModel, type UserTableModel } from './database';
|
||||
import { Op } from 'sequelize';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export class User {
|
||||
private id: string;
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
private get userWhere() {
|
||||
return {
|
||||
where: {
|
||||
[Op.or]: [{ id: this.id }, { username: this.id }]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await UserTable.destroy(this.userWhere);
|
||||
}
|
||||
|
||||
async subscriptions(): Promise<ChannelSubscriptionModel[]> {
|
||||
const subscriptions = await UserTable.findAll({
|
||||
where: {
|
||||
userId: this.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!subscriptions) return [];
|
||||
|
||||
return subscriptions as unknown as ChannelSubscriptionModel[];
|
||||
}
|
||||
}
|
||||
|
||||
export type CreateUser = {
|
||||
username: string;
|
||||
password: {
|
||||
hash: string;
|
||||
salt: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function createUser(user: CreateUser): Promise<User> {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
await UserTable.create({
|
||||
id,
|
||||
username: user.username,
|
||||
passwordHash: user.password.hash,
|
||||
passwordSalt: user.password.salt,
|
||||
created: new Date()
|
||||
});
|
||||
|
||||
return new User(id);
|
||||
}
|
||||
|
||||
export async function getUser(identifier: string): Promise<User> {
|
||||
const user = await UserTable.findOne({
|
||||
where: {
|
||||
[Op.or]: [{ id: identifier }, { username: identifier }]
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User does not exist');
|
||||
}
|
||||
|
||||
return new User((user as UserTableModel).id);
|
||||
}
|
||||
|
||||
export async function authenticateUser(username: string, passwordHash: string): Promise<User> {
|
||||
const user = await UserTable.findOne({
|
||||
where: {
|
||||
username
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User does not exist');
|
||||
}
|
||||
|
||||
const userModel = user as UserTableModel;
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
// Password is hashed in the browser, so if db is leaked it doesn't matter.
|
||||
// Timing safe equal used to stop timing attacks when comparing strings.
|
||||
if (
|
||||
!crypto.timingSafeEqual(
|
||||
textEncoder.encode(passwordHash),
|
||||
textEncoder.encode(userModel.passwordHash)
|
||||
)
|
||||
) {
|
||||
throw new Error('User does not exist');
|
||||
}
|
||||
|
||||
return new User(userModel.id);
|
||||
}
|
||||
Reference in New Issue
Block a user