Skip to content
Merged
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
59 changes: 59 additions & 0 deletions apps/server/src/app/barcode/barcodes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class BarcodesService {
scale?: number;
height?: number;
}): Promise<Buffer> {
this.validateStandardText(params.type, params.text);
try {
const { type, text, includetext = false, scale = 3, height } = params;
const opts: any = { bcid: type, text, includetext, scale };
Expand Down Expand Up @@ -54,6 +55,64 @@ export class BarcodesService {
}
}


// ---------------------------------------------------------------------------
// Standard barcode pre-validation (runs before bwip-js to give clear errors)
// ---------------------------------------------------------------------------

/**
* Validates the text payload for standard barcode types that have strict
* format requirements. Throws BadRequestException with a clear message so
* the bwip-js opaque error never reaches the client.
*/
private validateStandardText(type: StandardBarcodeType, text: string): void {
const t = text.trim();

switch (type) {
case StandardBarcodeType.EAN13: {
if (!/^\d{12,13}$/.test(t))
throw new BadRequestException('EAN-13 requires 12 or 13 digits (check digit is optional — it will be verified).');
if (t.length === 13 && !this.eanCheckDigitValid(t))
throw new BadRequestException(`EAN-13 check digit is wrong. Expected ${this.eanCheckDigit(t.slice(0, 12))}, got ${t[12]}.`);
break;
}
case StandardBarcodeType.EAN8: {
if (!/^\d{7,8}$/.test(t))
throw new BadRequestException('EAN-8 requires 7 or 8 digits (check digit is optional — it will be verified).');
if (t.length === 8 && !this.eanCheckDigitValid(t))
throw new BadRequestException(`EAN-8 check digit is wrong. Expected ${this.eanCheckDigit(t.slice(0, 7))}, got ${t[7]}.`);
break;
}
case StandardBarcodeType.UPCA: {
if (!/^\d{11,12}$/.test(t))
throw new BadRequestException('UPC-A requires 11 or 12 digits (check digit is optional — it will be verified).');
if (t.length === 12 && !this.eanCheckDigitValid(t))
throw new BadRequestException(`UPC-A check digit is wrong. Expected ${this.eanCheckDigit(t.slice(0, 11))}, got ${t[11]}.`);
break;
}
case StandardBarcodeType.ITF14: {
if (!/^\d{13,14}$/.test(t))
throw new BadRequestException('ITF-14 requires 13 or 14 digits.');
break;
}
// CODE128, CODE39, PDF417, DATAMATRIX accept arbitrary content — bwip handles it
}
}

/** Compute EAN/UPC check digit for a digit string (without check digit). */
private eanCheckDigit(digits: string): string {
let sum = 0;
for (let i = 0; i < digits.length; i++) {
sum += parseInt(digits[i], 10) * (i % 2 === 0 ? 1 : 3);
}
return String((10 - (sum % 10)) % 10);
}

/** Verify that the last character is the correct EAN check digit. */
private eanCheckDigitValid(full: string): boolean {
return this.eanCheckDigit(full.slice(0, -1)) === full[full.length - 1];
}

// ---------------------------------------------------------------------------
// GS1 — shared helpers
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,16 @@
type="text"
formControlName="text"
placeholder="Text / Numbers"
[class.is-invalid]="form.controls['text'].errors?.['barcodeFormat']"
/>
</label>
@if (form.controls['text'].errors?.['barcodeFormat']) {
<div class="invalid-feedback d-block small text-danger mt-1">
⚠️ {{ form.controls['text'].errors?.['barcodeFormat'] }}
</div>
} @else if (errorMsg) {
<div class="small text-danger mt-1">⚠️ {{ errorMsg }}</div>
}
</div>

<fieldset>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,65 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output, Input, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { AbstractControl, FormBuilder, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
import { LucideAngularModule, BarcodeIcon, Trash2Icon } from 'lucide-angular';
import { BarcodeService } from './barcode.service';
import { BarcodeRequest, StandardBarcodeType } from './models';
import { Subject, EMPTY } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, startWith, switchMap, takeUntil } from 'rxjs/operators';


/** EAN check-digit calculation (Luhn-style, same logic as backend). */
function eanCheckDigit(digits: string): string {
let sum = 0;
for (let i = 0; i < digits.length; i++) {
sum += parseInt(digits[i], 10) * (i % 2 === 0 ? 1 : 3);
}
return String((10 - (sum % 10)) % 10);
}

/**
* Returns a ValidatorFn that checks barcode text against the selected type.
* The validator is re-created each time the type changes via setValidators().
*/
function barcodeTextValidator(getType: () => string): ValidatorFn {
return (ctrl: AbstractControl): ValidationErrors | null => {
const text: string = (ctrl.value ?? '').trim();
if (!text) return null; // required is handled by Validators.required
const type = getType();

switch (type) {
case 'ean13': {
if (!/^\d{12,13}$/.test(text))
return { barcodeFormat: 'EAN-13: exactly 12 or 13 digits required' };
if (text.length === 13 && eanCheckDigit(text.slice(0, 12)) !== text[12])
return { barcodeFormat: `EAN-13: wrong check digit (expected ${eanCheckDigit(text.slice(0, 12))})` };
return null;
}
case 'ean8': {
if (!/^\d{7,8}$/.test(text))
return { barcodeFormat: 'EAN-8: exactly 7 or 8 digits required' };
if (text.length === 8 && eanCheckDigit(text.slice(0, 7)) !== text[7])
return { barcodeFormat: `EAN-8: wrong check digit (expected ${eanCheckDigit(text.slice(0, 7))})` };
return null;
}
case 'upca': {
if (!/^\d{11,12}$/.test(text))
return { barcodeFormat: 'UPC-A: exactly 11 or 12 digits required' };
if (text.length === 12 && eanCheckDigit(text.slice(0, 11)) !== text[11])
return { barcodeFormat: `UPC-A: wrong check digit (expected ${eanCheckDigit(text.slice(0, 11))})` };
return null;
}
case 'itf14': {
if (!/^\d{13,14}$/.test(text))
return { barcodeFormat: 'ITF-14: exactly 13 or 14 digits required' };
return null;
}
default:
return null;
}
};
}

@Component({
standalone: true,
selector: 'app-barcode-editor-item',
Expand All @@ -26,6 +79,7 @@ export class BarcodeEditorItemComponent implements OnInit, OnDestroy {

previewUrl: string | null = null;
loading = false;
errorMsg: string | null = null;
private destroy$ = new Subject<void>();

form = this.fb.group({
Expand All @@ -37,21 +91,39 @@ export class BarcodeEditorItemComponent implements OnInit, OnDestroy {
});

ngOnInit(): void {
// Attach type-aware validator and re-run it when the type changes
const textCtrl = this.form.controls['text'];
const getType = () => this.form.controls['type'].value;
textCtrl.addValidators(barcodeTextValidator(getType));

this.form.controls['type'].valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
textCtrl.updateValueAndValidity();
});

this.form.valueChanges.pipe(
startWith(this.form.getRawValue()),
map(() => this.buildReq()),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
debounceTime(300),
switchMap(req => {
// Show validation error inline instead of firing the request
const formatErr = textCtrl.errors?.['barcodeFormat'];
if (formatErr) {
this.errorMsg = formatErr;
this.revokePreview();
return EMPTY;
}
this.errorMsg = null;
if (!this.form.valid || !req.text) {
this.revokePreview();
return EMPTY;
}
this.loading = true;
return this.api.preview$(req).pipe(
catchError(err => {
console.error(err);
this.errorMsg = err?.error?.message ?? err?.message ?? 'Preview failed';
this.revokePreview();
this.loading = false;
return EMPTY;
})
);
Expand Down
Loading