Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
SECRET_KEY=KEY
SECRET_KEY=KEY
MONGODB_URI=mongodb://127.0.0.1:27017/benderirc
8 changes: 0 additions & 8 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ import { mongo } from "mongoose";
const UserSettings = {
// Listed as BOT but is actually a user
// each "Setting" is a nice that will sit on the server and proxied through the websocket.
nick : 'nick',
username: 'El encuentro',
gecos: 'El encuentro',
password : '',
realname : 'El encuentro',
channels : [],
host : 'irc.server.com',
port : 6667,
auto_reconnect: false,
auto_reconnect_wait: 10000,
auto_reconnect_max_retries: 1,
Expand Down
14 changes: 14 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
// Automatically clear mock calls and instances between every test
clearMocks: true,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// A list of paths to directories that Jest should use to search for files in
roots: ['<rootDir>/tests'],
// The glob patterns Jest uses to detect test files
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
};
201 changes: 117 additions & 84 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,117 @@
import Express from "express";
import Cors from "cors";
import morgan from "morgan";
import UserSettings from "./config";
import MongooseDal from "./services/mongo";
import { IChannel } from "./models/channel";
import { routes } from "./router";
import { createServer } from "http";
import { SocketService } from "./services/socket";
import { connect } from "./router/chanserv";
import IrcService from "./services/ircService";
import { isLoggedIn } from "./middleware/auth";

MongooseDal.connect().then(
() => {
console.log("Connected to MongoDB");
},
(err) => {
console.log(err);
}
);

const app = Express();

const httpServer = createServer(app);
const socketService = new SocketService(httpServer);
const clientService = new IrcService(socketService);

const client = clientService.getClient();

socketService.configureClient();

app.use(Cors());
app.use(Express.json());
app.use(Express.urlencoded({ extended: true }));
app.use(morgan("dev"));

app.use("/", routes);
app.post("/connect", isLoggedIn, connect(clientService));

app.use("/static", Express.static("public"));
app.use("/login", Express.static("public/login/login.html"));

app.post("/join/dm/:nick", isLoggedIn, async (req, res) => {
const nick = req.params.nick;
var dms = await MongooseDal.getDirectMessagesForUser(UserSettings.nick, nick);
res.send({ nick: nick, messages: dms?.messages || [] });
});

app.post("/channel/join", isLoggedIn, async (req, res) => {
const socketConnections = socketService.getConnections();
const channel = client.channel("#" + req.body.channel, req.body.key);

channel.join("#" + req.body.channel, req.body.key);

let channelMongo: IChannel = {
active: true,
name: req.body.channel,
description: "Test Channel",
owner: UserSettings.nick,
created_at: new Date(),
updated_at: new Date(),
messages: [],
};

var channelMessages = await MongooseDal.getMessagesForChannel(req.body.channel);
await MongooseDal.createChannel(channelMongo);

channel.updateUsers(() => {
var users = channel.users;
socketConnections.forEach((socket) => {
socket.socket.emit("channel:joined", {
channel: req.body.channel,
users: users,
messages: channelMessages?.messages || [],
});
});
res.send({ users: users, channel: req.body.channel });
});
});

httpServer.listen(3000, () => {
console.log("listening on *:3000");
});
import Express from "express";
import Cors from "cors";
import morgan from "morgan";
import MongooseDal from "./services/mongo";
import { IChannel } from "./models/channel";
import { routes } from "./router"; // This will include the ircRouter via router/index.ts
import { createServer } from "http";
import { SocketService } from "./services/socket";
import IrcService from "./services/ircService";
import { isLoggedIn } from "./middleware/auth";

MongooseDal.connect().then(
() => {
console.log("Connected to MongoDB");
},
(err) => {
console.log(err);
}
);

const app = Express();

const httpServer = createServer(app);
const socketService = new SocketService(httpServer);
// Export clientService so it can be imported by router/irc.ts
export const clientService = new IrcService(socketService);
socketService.configureClient();

app.use(Cors());
app.use(Express.json());
app.use(Express.urlencoded({ extended: true }));
app.use(morgan("dev"));

// Prefix all routes from router/index.ts with /api
app.use("/api", routes);

app.use("/static", Express.static("public"));
app.use("/login", Express.static("public/login/login.html"));

app.post("/join/dm/:nick", isLoggedIn, async (req, res) => {
const nick = req.params.nick;
var dms = await MongooseDal.getDirectMessagesForUser("tes", nick); // TODO: "tes" should be dynamic userId
res.send({ nick: nick, messages: dms?.messages || [] });
});

app.post("/channel/join", isLoggedIn, async (req: any, res) => { // Using 'any' for req for now, can be typed better
try {
// 1. Middleware and User ID
if (!req.user || !req.user.user || !req.user.user._id) {
return res.status(401).json({ message: "User not authenticated or user ID missing." });
}
const userId = req.user.user._id;
const userNick = req.user.user.username || "UnknownUser"; // Fallback for owner

// 2. Request Body Parameters
const { serverName, channel, key } = req.body;

if (!serverName || typeof serverName !== 'string' || serverName.trim() === '') {
return res.status(400).json({ message: "serverName is required and must be a non-empty string." });
}
if (!channel || typeof channel !== 'string' || channel.trim() === '') {
return res.status(400).json({ message: "channel is required and must be a non-empty string." });
}

// 3. Call clientService.joinChannel
// Prepending "#" is handled by the IrcService.joinChannel if it's a convention there,
// or it should be prepended here if IrcService expects it without the #.
// Based on previous subtask, IrcService.joinChannel does not add "#", so we add it here.
// However, the new IrcService.joinChannel in the previous step doesn't show it adding "#".
// Let's assume for now that the channel name should be passed as is, and if it needs "#", IrcService handles it or it's part of the name.
// For consistency with typical IRC, we often see "#" prepended by the client initiating the join.
// The previous implementation `client.channel("#" + req.body.channel...` did prepend it.
// The IrcService.joinChannel method itself does *not* prepend '#'.
// So, it's better to prepend it here before calling the service.
const channelWithHash = channel.startsWith("#") ? channel : "#" + channel;

const joined = await clientService.joinChannel(userId, serverName, channelWithHash, key);

if (!joined) {
return res.status(500).json({ message: `Failed to join channel ${channelWithHash} on server ${serverName}. Client may not be connected or channel join failed.` });
}

// 4. Database and Response Logic (if joinChannel is successful)
let channelMongo: IChannel = {
active: true,
name: channel, // Store the original channel name without hash for DB consistency if preferred
description: "User Joined Channel", // Or some other default/dynamic description
owner: userNick,
created_at: new Date(),
updated_at: new Date(),
messages: [],
};

var channelMessages = await MongooseDal.getMessagesForChannel(channel); // Use original channel name
await MongooseDal.createChannel(channelMongo); // This might create duplicates if channel already exists. Consider findOrCreate.

const socketConnections = socketService.getConnections();
socketConnections.forEach((socket) => {
socket.socket.emit("channel:joined", {
channel: channel, // Use original channel name
users: [], // TODO: Implement a mechanism to fetch/update user list for the channel after joining.
messages: channelMessages?.messages || [],
});
});

res.status(200).json({ channel: channel, message: "Join initiated. User list will be updated." });

} catch (error) {
console.error("Error in /channel/join:", error);
const message = (error instanceof Error) ? error.message : 'An unexpected error occurred.';
res.status(500).json({ message: "Failed to join channel due to an internal error.", error: message });
}
});

httpServer.listen(3000, () => {
console.log("listening on *:3000");
});
8 changes: 3 additions & 5 deletions middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import dotenv from 'dotenv';
dotenv.config();

import jwt from "jsonwebtoken";
import logger from "mercedlogger";
import { log } from "mercedlogger";

const SECRET_KEY = process.env.SECRET_KEY ? process.env.SECRET_KEY : "SECRET_KEY";

const isLoggedIn = async (req, res, next) => {
try {
logger.log.magenta("Checking for token");
console.log(req.headers);
logger.log.magenta("Auth Header", req.headers);
log.magenta("Checking for token");

if (!req.headers.authorization){
res.status(401).json({ error: "No authorization header" });
Expand Down Expand Up @@ -38,7 +36,7 @@ const isAdmin = async (req, res, next) => {
const token = req.query.admin;
try {
// TODO: check if user is admin
logger.log.magenta("ADMIN BEING PASSED THROUGH", token);
log.magenta("ADMIN BEING PASSED THROUGH", token);
if(token === "admin") {
next();
}
Expand Down
25 changes: 23 additions & 2 deletions models/user.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import { Schema, model, connect } from 'mongoose';

export interface IIrcServer { // Add export here
name: string;
host: string;
port: number;
nick: string;
password?: string;
realname?: string;
channels?: string[];
}

interface IUser {
username: string;
password: string;
email: string;
created_at: Date;
updated_at: Date;
ircServers?: IIrcServer[];
}

const userSchema = new Schema<IUser>({
username: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true },
created_at: { type: Date, default: Date.now },
updated_at: { type: Date, default: Date.now }
updated_at: { type: Date, default: Date.now },
ircServers: [{
name: { type: String, required: true },
host: { type: String, required: true },
port: { type: Number, required: true },
nick: { type: String, required: true },
password: { type: String },
realname: { type: String },
channels: [{ type: String }]
}]
});

const User = model<IUser>('User', userSchema);

export default User;
export default User;
export { IUser }; // Export interfaces for use in other modules
41 changes: 0 additions & 41 deletions models/userSchema.ts

This file was deleted.

Loading