diff --git a/AUTH_README.md b/AUTH_README.md index 0d08a4b..60fa729 100644 --- a/AUTH_README.md +++ b/AUTH_README.md @@ -2,7 +2,7 @@ ## Overview -This API implements a simplified token-based authentication system. The existing root endpoint (`POST /`) remains tokenless for backward compatibility, while a new secured endpoint (`POST /identify`) requires authentication. +This API implements a simple token-based authentication system. ## Authentication System @@ -13,9 +13,6 @@ This API implements a simplified token-based authentication system. The existing ### Endpoints -#### Legacy Endpoint (No Authentication) -- `POST /` - Species identification (legacy, no token required) - #### Secured Endpoints - `POST /identify` - Species identification (requires API token or admin token) - All other endpoints require admin token @@ -35,7 +32,8 @@ curl -X POST https://your-api-domain.com/admin/tokens \ -d '{ "name": "New Client Name", "application": "my-app", - "description": "Token for my application" + "description": "Token for my application", + "model": "Norwegian" }' # Enable a token (using token prefix) @@ -82,7 +80,7 @@ API tokens are managed in `auth/tokens.json`. Each token must be associated with { "your-api-token-1": { "name": "Client Name 1", - "application": "artsorakelet", + "application": "artsorakel", "enabled": true, "created": "2024-01-01T00:00:00Z", "description": "Description of what this token is for" @@ -92,7 +90,8 @@ API tokens are managed in `auth/tokens.json`. Each token must be associated with "application": "research-tool", "enabled": false, "created": "2024-01-01T00:00:00Z", - "description": "Another client token (disabled)" + "description": "Another client token (disabled)", + "model": "Swedish" } } ``` @@ -100,9 +99,12 @@ API tokens are managed in `auth/tokens.json`. Each token must be associated with **Required Fields:** - `name`: Human-readable name for the token - `application`: Application identifier used for logging and identification + +**Optional Fields:** - `enabled`: Boolean indicating if the token is active (defaults to true) - `created`: Timestamp when the token was created -- `description`: Optional description of the token's purpose +- `description`: Description of the token's purpose +- `model`: Restrict token to a specific model (`Norwegian`, `Swedish`, or `European`). If not set, model is selected based on location/IP. ## Token Management @@ -116,7 +118,7 @@ curl -X POST https://your-api-domain.com/admin/tokens \ -H "Content-Type: application/json" \ -d '{ "name": "Mobile App Client", - "application": "artsorakelet-mobile", + "application": "artsorakel-mobile", "description": "Token for the mobile application" }' ``` @@ -127,13 +129,33 @@ curl -X POST https://your-api-domain.com/admin/tokens \ "message": "Token created successfully", "token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6", "name": "Mobile App Client", - "application": "artsorakelet-mobile", + "application": "artsorakel-mobile", "enabled": true, "created": "2024-01-15T10:30:00Z", "warning": "Store this token securely. It will not be shown again in full." } ``` +### Model-Restricted Tokens + +Tokens can be restricted to use a specific identification model regardless of location. This is useful for applications that should always use a particular regional model. + +```bash +curl -X POST https://your-api-domain.com/admin/tokens \ + -H "Authorization: Bearer your-admin-token" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Swedish Partner App", + "application": "swedish-partner", + "description": "Token restricted to Swedish model", + "model": "Swedish" + }' +``` + +**Valid model values:** `Norwegian`, `Swedish`, `European` + +When a model-restricted token is used, the response `modelInfo.locationSource` will be `"token"` instead of `"coordinates"` or `"ip"`. + ### Managing Token Status **Enable a token:** diff --git a/config/constants.js b/config/constants.js index 40a64fe..614dc37 100644 --- a/config/constants.js +++ b/config/constants.js @@ -10,6 +10,7 @@ const pictureFile = `${cachedir}/taxonPictures.json`; const uploadsdir = "./uploads"; const listVersionsFile = `${cachedir}/listversions.json`; const TOKENS_FILE = "./auth/tokens.json"; +const VALID_MODELS = ['Norwegian', 'Swedish', 'European']; const headfile = ".git/HEAD"; @@ -66,6 +67,7 @@ module.exports = { uploadsdir, listVersionsFile, TOKENS_FILE, + VALID_MODELS, branch, server_url, warningsConfig, diff --git a/jobs/cron.js b/jobs/cron.js index 38036a1..1bf8a94 100644 --- a/jobs/cron.js +++ b/jobs/cron.js @@ -16,9 +16,8 @@ const setupCronJobs = () => { if (time_between >= survival_length) { fs.unlink(`${uploadsdir}/${file}`, (err) => { if (err) { - console.log("could not delete file"); + console.log("could not delete file: " + filename); } - console.log("The file has been deleted!"); }); } }); diff --git a/middleware/auth.js b/middleware/auth.js index 51de06e..9af9cc7 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -109,7 +109,8 @@ const authenticateApiToken = (req, res, next) => { type: "api", token: token, name: validTokens[token].name, - application: validTokens[token].application + application: validTokens[token].application, + model: validTokens[token].model }; return next(); } diff --git a/routes/admin.js b/routes/admin.js index 67d72a2..4d2adc7 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -10,19 +10,25 @@ const { } = require("../middleware/auth"); const { writeErrorLog } = require("../services/logging"); const { taxadir } = require("../services/taxon"); -const { logdir } = require("../config/constants"); +const { logdir, VALID_MODELS } = require("../config/constants"); module.exports = (app, upload) => { app.get("/admin/tokens", authLimiter, authenticateAdminToken, (req, res) => { try { const validTokens = getValidTokens(); - const tokenList = Object.keys(validTokens).map((token) => ({ - token: token.substring(0, 8) + "...", - name: validTokens[token].name, - application: validTokens[token].application, - enabled: validTokens[token].enabled, - created: validTokens[token].created, - })); + const tokenList = Object.keys(validTokens).map((token) => { + const tokenInfo = { + token: token.substring(0, 8) + "...", + name: validTokens[token].name, + application: validTokens[token].application, + enabled: validTokens[token].enabled, + created: validTokens[token].created, + }; + if (validTokens[token].model) { + tokenInfo.model = validTokens[token].model; + } + return tokenInfo; + }); res.status(200).json({ count: tokenList.length, tokens: tokenList, @@ -49,7 +55,7 @@ module.exports = (app, upload) => { app.post("/admin/tokens", authLimiter, authenticateAdminToken, (req, res) => { try { - const { name, application, description } = req.body; + const { name, application, description, model } = req.body; if (!name || !application) { return res.status(400).json({ @@ -58,6 +64,13 @@ module.exports = (app, upload) => { }); } + if (model && !VALID_MODELS.includes(model)) { + return res.status(400).json({ + error: "Bad request", + message: `Invalid model. Valid options: ${VALID_MODELS.join(', ')}`, + }); + } + const newToken = generateSecureToken(); const validTokens = getValidTokens(); @@ -69,6 +82,10 @@ module.exports = (app, upload) => { description: description ? description.trim() : `Token for ${name}`, }; + if (model) { + tokenData.model = model; + } + validTokens[newToken] = tokenData; if (!saveTokens()) { @@ -77,7 +94,7 @@ module.exports = (app, upload) => { }); } - res.status(201).json({ + const response = { message: "Token created successfully", token: newToken, name: tokenData.name, @@ -85,7 +102,13 @@ module.exports = (app, upload) => { enabled: tokenData.enabled, created: tokenData.created, warning: "Store this token securely. It will not be shown again in full.", - }); + }; + + if (model) { + response.model = model; + } + + res.status(201).json(response); writeErrorLog( `Token created successfully`, diff --git a/routes/identify.js b/routes/identify.js index 82c2e56..68dafca 100644 --- a/routes/identify.js +++ b/routes/identify.js @@ -62,7 +62,7 @@ module.exports = (app, upload) => { json.predictions[0].taxon = { vernacularName: "*** Utdatert versjon ***", name: - "Vennligst oppdater Artsorakelet via app store, eller Ctrl-Shift-R på pc", + "Vennligst oppdater Artsorakel via app store, eller Ctrl-Shift-R på pc", }; res.status(200).json(json); diff --git a/services/identification.js b/services/identification.js index e7b9c62..eaf64a6 100644 --- a/services/identification.js +++ b/services/identification.js @@ -61,8 +61,25 @@ const getId = async (req) => { let username = process.env.NATURALIS_USERNAME_NORWAY let password = process.env.NATURALIS_PASSWORD_NORWAY + const tokenModelRestriction = req.auth?.model; + + const selectModelCredentials = (model) => { + if (model === 'Swedish') { + username = process.env.NATURALIS_USERNAME_SWEDEN; + password = process.env.NATURALIS_PASSWORD_SWEDEN; + return process.env.NATURALIS_TOKEN_SWEDEN; + } else if (model === 'Norwegian') { + return process.env.NATURALIS_TOKEN_NORWAY; + } else { + return process.env.NATURALIS_TOKEN_EUROPE; + } + }; - if (receivedParams.includes('model') && req.body.model && req.body.model.toLowerCase() === "global") { + if (tokenModelRestriction) { + token = selectModelCredentials(tokenModelRestriction); + modelUsed = tokenModelRestriction; + locationSource = 'token'; + } else if (receivedParams.includes('model') && req.body.model && req.body.model.toLowerCase() === "global") { token = process.env.NATURALIS_TOKEN_EUROPE; modelUsed = 'European'; } else if (country === 'SE') {