feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+113
View File
@@ -0,0 +1,113 @@
const { AdminUser } = require('../models');
const jwt = require('jsonwebtoken');
const logger = require('../utils/logger');
class AdminController {
async login(req, res) {
try {
const { username, password } = req.body;
const user = await AdminUser.findOne({ where: { username } });
if (!user || !user.isActive) {
return res.status(401).json({ error: 'Ogiltiga inloggningsuppgifter' });
}
const isValid = await user.comparePassword(password);
if (!isValid) {
return res.status(401).json({ error: 'Ogiltiga inloggningsuppgifter' });
}
await user.update({ lastLogin: new Date() });
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
success: true,
data: {
token,
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
}
});
} catch (error) {
logger.error('Admin login failed:', error);
res.status(500).json({ error: 'Inloggning misslyckades' });
}
}
async createUser(req, res) {
try {
const { username, email, password, role } = req.body;
const user = await AdminUser.create({
username,
email,
password,
role: role || 'analyst'
});
res.status(201).json({
success: true,
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
logger.error('Create admin user failed:', error);
res.status(500).json({ error: 'Kunde inte skapa användare' });
}
}
async getStats(req, res) {
try {
const { Order, Report } = require('../models');
const totalOrders = await Order.count();
const completedOrders = await Order.count({ where: { status: 'completed' } });
const pendingOrders = await Order.count({ where: { status: 'pending' } });
const failedOrders = await Order.count({ where: { status: 'failed' } });
const totalReports = await Report.count();
// Recent activity
const recentOrders = await Order.findAll({
order: [['createdAt', 'DESC']],
limit: 5
});
res.json({
success: true,
data: {
orders: {
total: totalOrders,
completed: completedOrders,
pending: pendingOrders,
failed: failedOrders,
completionRate: totalOrders > 0 ? ((completedOrders / totalOrders) * 100).toFixed(1) : 0
},
reports: {
total: totalReports
},
recentOrders
}
});
} catch (error) {
logger.error('Get stats failed:', error);
res.status(500).json({ error: 'Kunde inte hämta statistik' });
}
}
}
module.exports = new AdminController();
+144
View File
@@ -0,0 +1,144 @@
const { Order } = require('../models');
const reportQueue = require('../queues/reportQueue');
const logger = require('../utils/logger');
class OrderController {
async create(req, res) {
try {
const order = await Order.create({
productName: req.body.productName,
productType: req.body.productType,
ingredients: req.body.ingredients,
usage: req.body.usage,
format: req.body.format,
priceLevel: req.body.priceLevel,
certifications: req.body.certifications,
targetGroups: req.body.targetGroups || [],
targetMarkets: req.body.targetMarkets || [],
currentMarkets: req.body.currentMarkets || [],
email: req.body.email,
priority: req.body.priority || 'normal',
estimatedCompletion: new Date(Date.now() + 72 * 60 * 60 * 1000) // 72 hours
});
// Add to queue for processing
await reportQueue.add({
orderId: order.id
}, {
delay: 1000, // Start after 1 second
priority: req.body.priority === 'urgent' ? 1 :
req.body.priority === 'high' ? 2 : 3
});
logger.info(`Order created: ${order.id}`);
res.status(201).json({
success: true,
data: {
id: order.id,
productName: order.productName,
status: order.status,
progress: order.progress,
estimatedCompletion: order.estimatedCompletion,
message: 'Din beställning har mottagits och bearbetas.'
}
});
} catch (error) {
logger.error('Order creation failed:', error);
res.status(500).json({
error: 'Kunde inte skapa beställning',
message: error.message
});
}
}
async getById(req, res) {
try {
const order = await Order.findByPk(req.params.id);
if (!order) {
return res.status(404).json({ error: 'Beställning hittades inte' });
}
res.json({
success: true,
data: order
});
} catch (error) {
logger.error('Get order failed:', error);
res.status(500).json({ error: 'Kunde inte hämta beställning' });
}
}
async getByEmail(req, res) {
try {
const orders = await Order.findAll({
where: { email: req.query.email },
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: orders
});
} catch (error) {
logger.error('Get orders by email failed:', error);
res.status(500).json({ error: 'Kunde inte hämta beställningar' });
}
}
async getAll(req, res) {
try {
const { status, page = 1, limit = 20 } = req.query;
const where = {};
if (status) where.status = status;
const orders = await Order.findAndCountAll({
where,
order: [['createdAt', 'DESC']],
limit: parseInt(limit),
offset: (parseInt(page) - 1) * parseInt(limit)
});
res.json({
success: true,
data: orders.rows,
pagination: {
total: orders.count,
page: parseInt(page),
pages: Math.ceil(orders.count / parseInt(limit))
}
});
} catch (error) {
logger.error('Get all orders failed:', error);
res.status(500).json({ error: 'Kunde inte hämta beställningar' });
}
}
async updateStatus(req, res) {
try {
const order = await Order.findByPk(req.params.id);
if (!order) {
return res.status(404).json({ error: 'Beställning hittades inte' });
}
await order.update({
status: req.body.status,
progress: req.body.progress,
progressMessage: req.body.progressMessage
});
res.json({
success: true,
data: order
});
} catch (error) {
logger.error('Update order status failed:', error);
res.status(500).json({ error: 'Kunde inte uppdatera status' });
}
}
}
module.exports = new OrderController();
+100
View File
@@ -0,0 +1,100 @@
const { Report, Order } = require('../models');
const logger = require('../utils/logger');
class ReportController {
async getById(req, res) {
try {
const report = await Report.findByPk(req.params.id, {
include: [{
model: Order,
as: 'order',
attributes: ['productName', 'productType', 'email', 'status']
}]
});
if (!report) {
return res.status(404).json({ error: 'Rapport hittades inte' });
}
res.json({
success: true,
data: report
});
} catch (error) {
logger.error('Get report failed:', error);
res.status(500).json({ error: 'Kunde inte hämta rapport' });
}
}
async getByOrderId(req, res) {
try {
const report = await Report.findOne({
where: { orderId: req.params.orderId },
include: [{
model: Order,
as: 'order'
}]
});
if (!report) {
return res.status(404).json({ error: 'Rapport hittades inte' });
}
res.json({
success: true,
data: report
});
} catch (error) {
logger.error('Get report by order failed:', error);
res.status(500).json({ error: 'Kunde inte hämta rapport' });
}
}
async getHtml(req, res) {
try {
const report = await Report.findByPk(req.params.id);
if (!report || !report.htmlContent) {
return res.status(404).json({ error: 'Rapport hittades inte' });
}
res.setHeader('Content-Type', 'text/html');
res.send(report.htmlContent);
} catch (error) {
logger.error('Get report HTML failed:', error);
res.status(500).json({ error: 'Kunde inte hämta rapport' });
}
}
async getAll(req, res) {
try {
const { page = 1, limit = 20 } = req.query;
const reports = await Report.findAndCountAll({
include: [{
model: Order,
as: 'order',
attributes: ['productName', 'email', 'status']
}],
order: [['createdAt', 'DESC']],
limit: parseInt(limit),
offset: (parseInt(page) - 1) * parseInt(limit)
});
res.json({
success: true,
data: reports.rows,
pagination: {
total: reports.count,
page: parseInt(page),
pages: Math.ceil(reports.count / parseInt(limit))
}
});
} catch (error) {
logger.error('Get all reports failed:', error);
res.status(500).json({ error: 'Kunde inte hämta rapporter' });
}
}
}
module.exports = new ReportController();