diff --git a/backend/package-lock.json b/backend/package-lock.json index 440cd9d..7250187 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,17 +1,21 @@ { - "name": "backend", + "name": "iptv-player-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "backend", + "name": "iptv-player-backend", "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/axios": "^0.9.36", "@types/express": "^5.0.0", "@types/node": "^22.10.2", + "axios": "^1.7.9", "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", @@ -131,6 +135,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "license": "MIT" }, + "node_modules/@types/axios": { + "version": "0.9.36", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", + "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", + "license": "MIT" + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -260,6 +270,12 @@ "@types/send": "*" } }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -365,6 +381,23 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -574,6 +607,23 @@ "fsevents": "~2.3.2" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-highlight": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", @@ -781,6 +831,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -872,6 +934,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1155,6 +1226,26 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -1171,6 +1262,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1537,6 +1642,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.17", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.17.tgz", + "integrity": "sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2090,6 +2201,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -2936,6 +3053,15 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "license": "MIT" }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index 713ec47..50adf87 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,13 +10,21 @@ "typeorm": "typeorm-ts-node-commonjs", "test": "echo \"Error: no test specified\" && exit 1" }, - "keywords": ["iptv", "streaming", "video"], + "keywords": [ + "iptv", + "streaming", + "video" + ], "author": "", "license": "ISC", "dependencies": { + "@types/axios": "^0.9.36", "@types/express": "^5.0.0", "@types/node": "^22.10.2", + "axios": "^1.7.9", "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts new file mode 100644 index 0000000..6a533e8 --- /dev/null +++ b/backend/src/config/database.ts @@ -0,0 +1,14 @@ +import { Pool } from 'pg'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, +}); + +export default pool; \ No newline at end of file diff --git a/backend/src/controllers/ContentController.ts b/backend/src/controllers/ContentController.ts index 22003c8..35299b5 100644 --- a/backend/src/controllers/ContentController.ts +++ b/backend/src/controllers/ContentController.ts @@ -8,62 +8,64 @@ export class ContentController { private favoriteRepository = AppDataSource.getRepository(Favorite); private watchHistoryRepository = AppDataSource.getRepository(WatchHistory); - async getLiveStreams(req: Request, res: Response) { + async getLiveStreams(req: Request, res: Response): Promise { try { const xtreamService = new XtreamService(req.user!); - const streams = await xtreamService.getLiveStreams(); + const response = await xtreamService.getLiveStreams(); + const streams = response as any[]; - if (!req.user!.adult_content_enabled) { - // Filter out adult content - streams = streams.filter((stream: any) => - !stream.category_name?.toLowerCase().includes('adult') && - !stream.name?.toLowerCase().includes('adult') - ); - } - - res.json(streams); + const filteredStreams = !req.user!.adult_content_enabled + ? streams.filter(stream => + !stream.category_name?.toLowerCase().includes('adult') && + !stream.name?.toLowerCase().includes('adult') + ) + : streams; + + res.json(filteredStreams); } catch (error) { res.status(500).json({ error: 'Error fetching live streams' }); } } - async getVodStreams(req: Request, res: Response) { + async getVodStreams(req: Request, res: Response): Promise { try { const xtreamService = new XtreamService(req.user!); - const streams = await xtreamService.getVodStreams(); + const response = await xtreamService.getVodStreams(); + const streams = response as any[]; - if (!req.user!.adult_content_enabled) { - streams = streams.filter((stream: any) => - !stream.category_name?.toLowerCase().includes('adult') && - !stream.name?.toLowerCase().includes('adult') - ); - } - - res.json(streams); + const filteredStreams = !req.user!.adult_content_enabled + ? streams.filter(stream => + !stream.category_name?.toLowerCase().includes('adult') && + !stream.name?.toLowerCase().includes('adult') + ) + : streams; + + res.json(filteredStreams); } catch (error) { res.status(500).json({ error: 'Error fetching VOD streams' }); } } - async getSeriesStreams(req: Request, res: Response) { + async getSeriesStreams(req: Request, res: Response): Promise { try { const xtreamService = new XtreamService(req.user!); - const series = await xtreamService.getSeriesStreams(); + const response = await xtreamService.getSeriesStreams(); + const series = response as any[]; - if (!req.user!.adult_content_enabled) { - series = series.filter((show: any) => - !show.category_name?.toLowerCase().includes('adult') && - !show.name?.toLowerCase().includes('adult') - ); - } - - res.json(series); + const filteredSeries = !req.user!.adult_content_enabled + ? series.filter(show => + !show.category_name?.toLowerCase().includes('adult') && + !show.name?.toLowerCase().includes('adult') + ) + : series; + + res.json(filteredSeries); } catch (error) { res.status(500).json({ error: 'Error fetching series' }); } } - async getSeriesInfo(req: Request, res: Response) { + async getSeriesInfo(req: Request, res: Response): Promise { try { const { seriesId } = req.params; const xtreamService = new XtreamService(req.user!); @@ -74,7 +76,7 @@ export class ContentController { } } - async getEPG(req: Request, res: Response) { + async getEPG(req: Request, res: Response): Promise { try { const { streamId } = req.params; const xtreamService = new XtreamService(req.user!); @@ -85,7 +87,7 @@ export class ContentController { } } - async addToFavorites(req: Request, res: Response) { + async addToFavorites(req: Request, res: Response): Promise { try { const { content_id, content_type, title, poster_url } = req.body; @@ -98,7 +100,8 @@ export class ContentController { }); if (existingFavorite) { - return res.status(400).json({ error: 'Already in favorites' }); + res.status(400).json({ error: 'Already in favorites' }); + return; } const favorite = this.favoriteRepository.create({ @@ -116,7 +119,7 @@ export class ContentController { } } - async getFavorites(req: Request, res: Response) { + async getFavorites(req: Request, res: Response): Promise { try { const favorites = await this.favoriteRepository.find({ where: { user: { id: req.user!.id } }, @@ -128,7 +131,7 @@ export class ContentController { } } - async removeFromFavorites(req: Request, res: Response) { + async removeFromFavorites(req: Request, res: Response): Promise { try { const { id } = req.params; await this.favoriteRepository.delete({ @@ -141,7 +144,7 @@ export class ContentController { } } - async updateWatchHistory(req: Request, res: Response) { + async updateWatchHistory(req: Request, res: Response): Promise { try { const { content_id, @@ -186,7 +189,7 @@ export class ContentController { } } - async getWatchHistory(req: Request, res: Response) { + async getWatchHistory(req: Request, res: Response): Promise { try { const watchHistory = await this.watchHistoryRepository.find({ where: { user: { id: req.user!.id } }, diff --git a/backend/src/controllers/UserController.ts b/backend/src/controllers/UserController.ts index f364bda..750798e 100644 --- a/backend/src/controllers/UserController.ts +++ b/backend/src/controllers/UserController.ts @@ -7,13 +7,14 @@ import jwt from 'jsonwebtoken'; export class UserController { private userRepository = AppDataSource.getRepository(User); - async register(req: Request, res: Response) { + async register(req: Request, res: Response): Promise { try { const { email, password, username } = req.body; const existingUser = await this.userRepository.findOne({ where: { email } }); if (existingUser) { - return res.status(400).json({ error: 'Email already in use' }); + res.status(400).json({ error: 'Email already in use' }); + return; } const hashedPassword = await bcrypt.hash(password, 10); @@ -35,18 +36,20 @@ export class UserController { } } - async login(req: Request, res: Response) { + async login(req: Request, res: Response): Promise { try { const { email, password } = req.body; const user = await this.userRepository.findOne({ where: { email } }); if (!user) { - return res.status(401).json({ error: 'Invalid credentials' }); + res.status(401).json({ error: 'Invalid credentials' }); + return; } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { - return res.status(401).json({ error: 'Invalid credentials' }); + res.status(401).json({ error: 'Invalid credentials' }); + return; } const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET!, { diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 25f1c82..445b37b 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -38,11 +38,12 @@ export const auth = async (req: Request, res: Response, next: NextFunction) => { } }; -export const adminAuth = async (req: Request, res: Response, next: NextFunction) => { +export const adminAuth = async (req: Request, res: Response, next: NextFunction): Promise => { try { await auth(req, res, () => { if (!req.user?.is_admin) { - return res.status(403).json({ error: 'Admin access required.' }); + res.status(403).json({ error: 'Admin access required.' }); + return; } next(); }); diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts new file mode 100644 index 0000000..b48e256 --- /dev/null +++ b/backend/src/routes/users.ts @@ -0,0 +1,42 @@ +import express from 'express'; +import axios from 'axios'; +import jwt from 'jsonwebtoken'; +import pool from '../config/database'; + +const router = express.Router(); + +router.post('/xtream-login', async (req, res) => { + const { xtream_username, xtream_password, xtream_url } = req.body; + + try { + // Test Xtream credentials + const xtreamResponse = await axios.get(`${xtream_url}/player_api.php`, { + params: { + username: xtream_username, + password: xtream_password, + }, + }); + + if (xtreamResponse.data.user_info) { + // Save user to database + const result = await pool.query( + 'INSERT INTO users (xtream_username, xtream_password, xtream_url) VALUES ($1, $2, $3) RETURNING *', + [xtream_username, xtream_password, xtream_url] + ); + + const user = result.rows[0]; + const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, { + expiresIn: '24h', + }); + + res.json({ user, token }); + } else { + res.status(401).json({ error: 'Invalid Xtream credentials' }); + } + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ error: 'Server error or invalid Xtream server' }); + } +}); + +export default router; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d9c5ad5..3c04a18 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,13 +1,12 @@ { - "name": "frontend", + "name": "iptv-player-frontend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frontend", + "name": "iptv-player-frontend", "version": "1.0.0", - "license": "ISC", "dependencies": { "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", @@ -18,6 +17,7 @@ "@types/react-dom": "^18.2.0", "@types/video.js": "^7.3.52", "axios": "^1.4.0", + "date-fns": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.1.1", @@ -1582,6 +1582,16 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5623b40..f24ce93 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,6 @@ "preview": "vite preview", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, - "dependencies": { "dependencies": { "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", @@ -20,6 +19,7 @@ "@types/react-dom": "^18.2.0", "@types/video.js": "^7.3.52", "axios": "^1.4.0", + "date-fns": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.1.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b9dddab..87e921d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,31 +1,35 @@ import { Routes, Route, Navigate } from 'react-router-dom'; import { Box } from '@mui/material'; +import { useSelector } from 'react-redux'; import Layout from './components/Layout'; -import Login from './pages/Login'; -import Register from './pages/Register'; +import XtreamLogin from './components/XtreamLogin'; import Home from './pages/Home'; import LiveTV from './pages/LiveTV'; import Movies from './pages/Movies'; import Series from './pages/Series'; import Favorites from './pages/Favorites'; import Profile from './pages/Profile'; -import PrivateRoute from './components/PrivateRoute'; +import { RootState } from './store'; function App() { + const { user } = useSelector((state: RootState) => state.auth); + return ( - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + {!user ? ( + } /> + ) : ( + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + )} ); diff --git a/frontend/src/components/PrivateRoute.tsx b/frontend/src/components/PrivateRoute.tsx deleted file mode 100644 index 80cd5f3..0000000 --- a/frontend/src/components/PrivateRoute.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Navigate } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { RootState } from '../store'; - -interface PrivateRouteProps { - children: React.ReactNode; -} - -const PrivateRoute: React.FC = ({ children }) => { - const { isAuthenticated } = useSelector((state: RootState) => state.auth); - - return isAuthenticated ? <>{children} : ; -}; - -export default PrivateRoute; \ No newline at end of file diff --git a/frontend/src/pages/Login.tsx b/frontend/src/components/XtreamLogin.tsx similarity index 51% rename from frontend/src/pages/Login.tsx rename to frontend/src/components/XtreamLogin.tsx index 780f5c6..90b0cbd 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/components/XtreamLogin.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { useNavigate, Link as RouterLink } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { Container, @@ -7,27 +6,35 @@ import { Typography, TextField, Button, - Link, Alert, } from '@mui/material'; import { auth } from '../services/api'; import { setCredentials } from '../store/slices/authSlice'; -const Login = () => { - const navigate = useNavigate(); +const XtreamLogin = () => { const dispatch = useDispatch(); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const [credentials, setCredentials] = useState({ + xtream_username: '', + xtream_password: '', + xtream_url: '', + }); const [error, setError] = useState(''); + const handleChange = (e: React.ChangeEvent) => { + setCredentials({ + ...credentials, + [e.target.name]: e.target.value, + }); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - const response = await auth.login({ email, password }); + const response = await auth.loginXtream(credentials); dispatch(setCredentials(response.data)); - navigate('/'); + setError(''); } catch (error: any) { - setError(error.response?.data?.error || 'An error occurred'); + setError(error.response?.data?.error || 'Invalid Xtream credentials or server error'); } }; @@ -41,10 +48,13 @@ const Login = () => { alignItems: 'center', }} > - - Sign in + + IPTV Player - + + Enter your Xtream credentials + + {error && ( {error} @@ -54,25 +64,30 @@ const Login = () => { margin="normal" required fullWidth - id="email" - label="Email Address" - name="email" - autoComplete="email" - autoFocus - value={email} - onChange={(e) => setEmail(e.target.value)} + name="xtream_url" + label="Xtream Server URL" + placeholder="http://example.com:port" + value={credentials.xtream_url} + onChange={handleChange} + /> + setPassword(e.target.value)} + value={credentials.xtream_password} + onChange={handleChange} /> - - - {"Don't have an account? Sign Up"} - - ); }; -export default Login; \ No newline at end of file +export default XtreamLogin; \ No newline at end of file diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 694c75c..2c0278c 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -27,34 +27,9 @@ const Profile = () => { const dispatch = useDispatch(); const { user } = useSelector((state: RootState) => state.auth); const { watchHistory } = useSelector((state: RootState) => state.content); - const [xtreamCredentials, setXtreamCredentials] = useState({ - xtream_username: user?.xtream_username || '', - xtream_password: user?.xtream_password || '', - xtream_url: user?.xtream_url || '', - }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); - const handleCredentialsChange = (e: React.ChangeEvent) => { - setXtreamCredentials({ - ...xtreamCredentials, - [e.target.name]: e.target.value, - }); - }; - - const handleCredentialsSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - try { - const response = await auth.updateXtreamCredentials(xtreamCredentials); - dispatch(setUser(response.data.user)); - setSuccess('Xtream credentials updated successfully'); - setError(''); - } catch (error: any) { - setError(error.response?.data?.error || 'Error updating credentials'); - setSuccess(''); - } - }; - const handleAdultContentToggle = async () => { try { const response = await auth.toggleAdultContent(); @@ -87,61 +62,7 @@ const Profile = () => { - - - - Xtream Credentials - - {error && ( - - {error} - - )} - {success && ( - - {success} - - )} -
- - - - - -
-
- - + Preferences diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx deleted file mode 100644 index 80ef326..0000000 --- a/frontend/src/pages/Register.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useState } from 'react'; -import { useNavigate, Link as RouterLink } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; -import { - Container, - Box, - Typography, - TextField, - Button, - Link, - Alert, -} from '@mui/material'; -import { auth } from '../services/api'; -import { setCredentials } from '../store/slices/authSlice'; - -const Register = () => { - const navigate = useNavigate(); - const dispatch = useDispatch(); - const [formData, setFormData] = useState({ - email: '', - username: '', - password: '', - confirmPassword: '', - }); - const [error, setError] = useState(''); - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ ...formData, [e.target.name]: e.target.value }); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (formData.password !== formData.confirmPassword) { - setError('Passwords do not match'); - return; - } - - try { - const response = await auth.register({ - email: formData.email, - username: formData.username, - password: formData.password, - }); - dispatch(setCredentials(response.data)); - navigate('/'); - } catch (error: any) { - setError(error.response?.data?.error || 'An error occurred'); - } - }; - - return ( - - - - Sign up - - - {error && ( - - {error} - - )} - - - - - - - - {'Already have an account? Sign In'} - - - - - - ); -}; - -export default Register; \ No newline at end of file diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 304cbf1..bff8219 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -13,15 +13,11 @@ api.interceptors.request.use((config) => { }); export const auth = { - register: async (data: { email: string; password: string; username: string }) => - api.post('/users/register', data), - login: async (data: { email: string; password: string }) => - api.post('/users/login', data), - updateXtreamCredentials: async (data: { + loginXtream: async (data: { xtream_username: string; xtream_password: string; xtream_url: string; - }) => api.put('/users/xtream-credentials', data), + }) => api.post('/users/xtream-login', data), toggleAdultContent: async () => api.post('/users/toggle-adult-content'), getProfile: async () => api.get('/users/profile'), }; diff --git a/frontend/src/store/slices/authSlice.ts b/frontend/src/store/slices/authSlice.ts index a437ab0..31aad39 100644 --- a/frontend/src/store/slices/authSlice.ts +++ b/frontend/src/store/slices/authSlice.ts @@ -1,13 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface User { - id: string; - email: string; - username: string; - xtream_username: string | null; - xtream_password: string | null; - xtream_url: string | null; - is_admin: boolean; + xtream_username: string; + xtream_password: string; + xtream_url: string; adult_content_enabled: boolean; } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1fa14f1..5c62ea7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -10,10 +10,10 @@ export default defineConfig({ }, }, server: { - port: 3000, + port: 5173, proxy: { '/api': { - target: 'http://localhost:4000', + target: 'http://localhost:5000', changeOrigin: true, }, },