Cara menggunakan mongodb unique values

Lompati ke konten utama

Browser ini sudah tidak didukung.

Mutakhirkan ke Microsoft Edge untuk memanfaatkan fitur, pembaruan keamanan, dan dukungan teknis terkini.

Menyimpan informasi pengguna aplikasi khusus di MongoDB

  • Artikel
  • 09/22/2022
  • 13 menit untuk membaca

Dalam artikel ini

Pada artikel ini, pelajari cara menambahkan database MongoDB ke API aplikasi web Statik. Sampai saat ini, informasi pengguna berasal dari platform Microsoft Identity menggunakan pustaka MSAL.js, atau dari Microsoft Graph. Artikel ini menambahkan langkah umum untuk menyimpan informasi pengguna khusus ke aplikasi web, yang tidak boleh disimpan di akun Identitas.

Untuk menyimpan data aplikasi web ini, khusus untuk pengguna, buat CosmosDB untuk sumber daya API MongoDB, dan gunakan database tersebut dengan paket npm mongoose.js. Semua kode yang diperlukan untuk menyelesaikan langkah ini tersedia dalam artikel ini.

Membuat sumber daya CosmosDB untuk API MongoDB

Gunakan ekstensi Visual Studio Code, Azure Databases, untuk membuat CosmosDB.

  1. Di Visual Studio Code, pilih ikon Azure untuk membuka penjelajah Azure.

  2. Dari penjelajah Azure, pilih + di bagian Azure Databases.

    Cara menggunakan mongodb unique values

  3. Ikuti permintaan yang tampil melalui tabel berikut, untuk memahami cara membuat sumber daya Azure CosmosDB Anda.

    PerintahNilai
    Pilih Azure Database Server Azure Cosmos DB untuk MongoDB API.
    Nama akun Masukkan nama akun, yang akan menjadi bagian dari string koneksi, seperti cosmosdb-mongodb-api-YOUR-ALIAS, mengganti YOUR-ALIAS dengan email atau alias perusahaan Anda.
    Pilih model kapasitas. Untuk tutorial sederhana dan penggunaan rendah ini, pilih throughputTanpa server
    Pilih grup sumber daya untuk sumber daya baru. Membuat grup sumber daya baru.
    Masukkan nama grup sumber daya baru. Terima nilai default, yang sama dengan nama akun yang Anda masukkan.
    Pilih lokasi untuk sumber daya baru. Pilih lokasi di wilayah geografis Anda.

Database aman dengan membatasi akses firewall

  1. Di penjelajah Azure, klik kanan sumber daya database baru, dan pilih Buka di portal.

  2. Dari bagian Pengaturan, pilih item menu Firewall dan jaringan virtual.

  3. Pilih Jaringan yang dipilih, kemudian pilih + Tambahkan IP saya saat ini.

  4. Pilih Terima koneksi dari dalam pusat data Azure publik. Dengan langkah ini, aplikasi web Statik dapat mengakses database Anda ketika dibuat.

    Cara menggunakan mongodb unique values

  5. Pilih Simpan. Database Anda saat ini hanya dapat diakses oleh workstation Anda.

    Anda dapat membiarkan browser terbuka ke sumber daya database. Saat Anda menambahkan data ke database, gunakan Data Explorer untuk melihat data tersebut.

Klien React: Tambahkan halaman dan formulir untuk input pengguna baru

Buat file baru, ./src/pages/FavoriteColor.jsx, dan salin kode berikut ke dalamnya untuk mengambil warna favorit pengguna.

import { useEffect, useState } from "react";
import { callOwnApiWithToken } from "../fetch";

export const FavoriteColor = ({ accessToken, endpoint, user, changeFunctionData }) => {

    const [color, setColor] = useState("");

    useEffect(() => {
            if(user && user.favoriteColor) {
                setColor(user.favoriteColor);
            }
    }, [user]);

    const onColorChange = (event) => {
        setColor(event.target.value);
    }

    const updateUserOnServer = async () => {
        const updateUser = await callOwnApiWithToken(accessToken, endpoint, {favoriteColor: color});
        changeFunctionData(updateUser);
    }

    const onFormSubmit = async (event) => {
        event.preventDefault();
        console.log('An color was submitted: ' + color);
        updateUserOnServer().then(response => setColor(response)).catch(error => console.log(error));
    }

    return (
        <> { user && 
            <center>
                <hr></hr>
                <h2>Your favorite Color?</h2>
                <form onSubmit={onFormSubmit}>
                    <input type="text" value={color} onChange={onColorChange} name="favoriteColor" />
                    <input type="submit" value="Submit" />
                </form>
                
            </center>
            }
        </>
    );
}

Ini adalah formulir untuk mengambil warna favorit pengguna.

Cara menggunakan mongodb unique values

Klien React: Tambahkan komponen FavoriteColor ke komponen Function

Tambahkan komponen FavoriteColor ke komponen Function dan tahan accessToken yang dikembalikan dari MSAL SDK, dalam status lokal.

Buka file, ./src/pages/Function.jsx, dan ganti kode dengan kode berikut untuk menyertakan komponen FavoriteColor baru.

import { useEffect, useState } from "react";

import { MsalAuthenticationTemplate, useMsal, useAccount } from "@azure/msal-react";
import { InteractionRequiredAuthError, InteractionType } from "@azure/msal-browser";

import { loginRequest, protectedResources } from "../authConfig";
import { callOwnApiWithToken } from "../fetch";
import { FunctionData } from "../components/DataDisplay";
import { FavoriteColor } from "./FavoriteColor";

const FunctionContent = () => {
    /**
     * useMsal is hook that returns the PublicClientApplication instance, 
     * an array of all accounts currently signed in and an inProgress value 
     * that tells you what msal is currently doing. For more, visit: 
     * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/hooks.md
     */
    const { instance, accounts, inProgress } = useMsal();
    const account = useAccount(accounts[0] || {});
    const [functionData, setFunctionData] = useState(null);
    const [accessToken, setAccessToken] = useState(null);

    useEffect(() => {
        if (account && inProgress === "none" && !functionData) {
            instance.acquireTokenSilent({
                scopes: protectedResources.functionApi.scopes,
                account: account
            }).then((response) => {
                setAccessToken(response.accessToken);
                callOwnApiWithToken(response.accessToken, protectedResources.functionApi.endpoint)
                    .then(response => setFunctionData(response));
            }).catch((error) => {
                // in case if silent token acquisition fails, fallback to an interactive method
                if (error instanceof InteractionRequiredAuthError) {
                    if (account && inProgress === "none") {
                        instance.acquireTokenPopup({
                            scopes: protectedResources.functionApi.scopes,
                        }).then((response) => {
                            setAccessToken(response.accessToken);
                            callOwnApiWithToken(response.accessToken, protectedResources.functionApi.endpoint)
                                .then(response => setFunctionData(response));
                        }).catch(error => console.log(error));
                    }
                }
            });
        }
    }, [account, inProgress, instance]);
  
    const changeFunctionData = (data) =>{
        setFunctionData(data);
    }

    return (
        <>
            { functionData ? <FunctionData functionData={functionData} /> : null }
            <FavoriteColor changeFunctionData={changeFunctionData} accessToken={accessToken} user={(functionData && functionData.response)? functionData.response: null} endpoint={protectedResources.functionApi.endpoint}/>
        </>
    );
};

/**
 * The `MsalAuthenticationTemplate` component will render its children if a user is authenticated 
 * or attempt to sign a user in. Just provide it with the interaction type you would like to use 
 * (redirect or popup) and optionally a [request object](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md)
 * to be passed to the login API, a component to display while authentication is in progress or a component to display if an error occurs. For more, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/getting-started.md
 */
export const Function = () => {
    const authRequest = {
        ...loginRequest
    };

    return (
        <MsalAuthenticationTemplate 
            interactionType={InteractionType.Redirect} 
            authenticationRequest={authRequest}
        >
            <FunctionContent />
        </MsalAuthenticationTemplate>
      )
};

Formulir warna favorit ditampilkan di bawah informasi token akses dari API Function.

Cara menggunakan mongodb unique values

Klien React: Tambahkan metode pengambilan baru dengan favoriteColor

Buka file, ./src/fetch.js, dan ganti metode callOwnApiWithToken di bagian bawah file dengan metode berikut.

export const callOwnApiWithToken = async(accessToken, apiEndpoint, user) => {
    return fetch(apiEndpoint, {
        method: "POST",
        body: JSON.stringify({
            ssoToken: accessToken,
            user
        })
    }).then(response => response.json())
        .catch(error => console.log(error));
}

Metode ini memanggil API Function dan meneruskan informasi pengguna khusus ke aplikasi web, seperti favoriteColor.

API Function: Menambahkan file mongoose

Sampel di sini menggunakan paket npm mongoose dan skema serta metode utilitas yang diperlukan untuk menyisipkan, memperbarui, dan menemukan informasi dalam database Cosmos DB.

  1. Di Visual Studio Code, klik kanan direktori API dari file explorer, kemudian pilih Buka di terminal terintegrasi.

  2. Di terminal, masukkan perintah berikut untuk menginstal paket npm mongoose.

    npm install mongoose
    
  3. Buat file skema mongoose baru di direktori API bernama user.model.js dan salin kode berikut ke dalamnya:

    const mongoose = require('mongoose');
    const validator = require('validator');
    
    const deleteAtPath = (obj, path, index) => {
        if (index === path.length - 1) {
            delete obj[path[index]];
            return;
        }
        deleteAtPath(obj[path[index]], path, index + 1);
    }
    const toJson = (schema) => {
        let transform;
        if (schema.options.toJSON && schema.options.toJSON.transform) {
            transform = schema.options.toJSON.transform;
        }
    
        schema.options.toJSON = Object.assign(schema.options.toJSON || {}, {
            transform(doc, ret, options) {
                Object.keys(schema.paths).forEach((path) => {
                    if (schema.paths[path].options && schema.paths[path].options.private) {
                        deleteAtPath(ret, path.split('.'), 0);
                    }
                });
    
                ret.id = ret._id.toString();
                delete ret._id;
                delete ret.__v;
                delete ret.createdAt;
                if (transform) {
                    return transform(doc, ret, options);
                }
            },
        });
    }
    
    const paginate = (schema) => {
        schema.statics.paginate = async function (filter, options) {
        let sort = '';
        if (options.sortBy) {
            const sortingCriteria = [];
            options.sortBy.split(',').forEach((sortOption) => {
            const [key, order] = sortOption.split(':');
            sortingCriteria.push((order === 'desc' ? '-' : '') + key);
            });
            sort = sortingCriteria.join(' ');
        } else {
            sort = 'createdAt';
        }
    
        const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10;
        const page = options.page && parseInt(options.page, 10) > 0 ? parseInt(options.page, 10) : 1;
        const skip = (page - 1) * limit;
    
        const countPromise = this.countDocuments(filter).exec();
        let docsPromise = this.find(filter).sort(sort).skip(skip).limit(limit);
    
        if (options.populate) {
            options.populate.split(',').forEach((populateOption) => {
            docsPromise = docsPromise.populate(
                populateOption
                .split('.')
                .reverse()
                .reduce((a, b) => ({ path: b, populate: a }))
            );
            });
        }
    
        docsPromise = docsPromise.exec();
    
        return Promise.all([countPromise, docsPromise]).then((values) => {
            const [totalResults, results] = values;
            const totalPages = Math.ceil(totalResults / limit);
            const result = {
            results,
            page,
            limit,
            totalPages,
            totalResults,
            };
            return Promise.resolve(result);
        });
        };
    };
    const userSchema = mongoose.Schema(
        {
            customAppId: {
                type: String,
                required: true,
                trim: true,
            },
            name: {
                type: String,
                required: true,
                trim: true,
            },
            email: {
                type: String,
                required: true,
                unique: true,
                trim: true,
                lowercase: true,
                validate(value) {
                    if (!validator.isEmail(value)) {
                        throw new Error('Invalid email');
                    }
                },
            },
            favoriteColor: {
                type: String,
                required: false,
                trim: true
            }
        },
        {
            timestamps: true,
        }
    );
    userSchema.plugin(toJson);
    userSchema.plugin(paginate);
    const User = mongoose.model('User', userSchema);
    module.exports = User;
    
  4. Buat file baru di direktori API bernama user.service.js dan salin kode berikut ke dalamnya. File ini menyediakan fungsionalitas untuk panggilan API Function index.js agar dapat tersambung ke Cosmos DB dengan SDK mongoose.

    const mongoose = require('mongoose');
    const User = require('./user.model');
    
    let connected = false;
    let connection = null;
    
    const mongooseConfig = {
        url: process.env.MONGODB_URL,
        options: {
            useCreateIndex: true,
            useNewUrlParser: true,
            useUnifiedTopology: true,
            useFindAndModify: false
        }
    }
    
    const connect = async () => {
        try {
            if (!connected && mongooseConfig && mongooseConfig.url && mongooseConfig.options) {
    
                // connect to DB
                connection = await mongoose.connect(mongooseConfig.url, mongooseConfig.options);
                connected = true;
                return connected;
    
            } else if (connected) {
    
                // already connected to DB
                console.log("Mongoose already connected");
                return connected;
            }
            else {
                // can't connect to DB
                throw Error("Mongoose URL needs to be added to Config settings as MONGODB_URL");
            }
        } catch (err) {
            console.log(`Mongoose connection error: ${err}`);
            throw Error({ name: "Sample-Mongoose", message: "connection error -" + error, status: 500 });
        }
    }
    const disconnect = () => {
        connection.disconnect();
    }
    
    const isConnected = () => {
        return connected;
    }
    
    const queryUsers = async (filter, options) => {
        const users = await User.paginate(filter, options);
        return users;
    }
    
    const getUserByEmail = async (email) => {
        if (email) {
            email = email.toLowerCase();
        }
        return await User.findOne({ email });
    }
    const upsertByEmail = async (email, mongodbUser) => {
    
        const query = { 'email': email };
    
        const tempUser = await User.findOneAndUpdate(query, mongodbUser, { upsert: true, new: true });
        return tempUser;
    
    }
    const getUserById = async (id) => {
        return await User.findById(id);
    };
    const deleteUserById = async (userId) => {
    
        const tempUser = await getUserById(userId);
        if (!tempUser) {
            throw new Error('User not found');
        }
        await User.remove();
        return tempUser;
    }
    
    module.exports = {
        connect,
        disconnect,
        isConnected,
        queryUsers,
        getUserById,
        getUserByEmail,
        upsertByEmail,
        deleteUserById
    }
    

API Function: Perbarui API untuk tersambung ke database

Buka file, ./api/HelloUser/index.js, dan ganti kode dengan yang berikut ini untuk mengambil ID unik pengguna favoriteColor dan aplikasi Active Directory untuk pengguna, customAppId.

Jangan mengubah objek config di bagian atas file.

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const msal = require('@azure/msal-node');
const fetch = require('node-fetch');
const UserService = require("../user.service");

// Before running the sample, you will need to replace the values in the .env file, 
const config = {
    auth: {
        clientId: process.env['CLIENT_ID'],
        authority: `https://login.microsoftonline.com/${process.env['TENANT_INFO']}`,
        clientSecret: process.env['CLIENT_SECRET'],
    }
};

// Create msal application object
const cca = new msal.ConfidentialClientApplication(config);

let aadAppUniqueUser=null;

module.exports = async function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');

    try {
        // get ssoToken from client request
        const ssoToken = (req.body && req.body.ssoToken);
        if (!ssoToken) throw Error({ name: "Sample-Auth", message: "no ssoToken sent from client", "status": 401 });

        // get appUser from client request
        // this isn't passed in on first request
        const favoriteColor = (req.body && req.body.user && req.body.user.favoriteColor) ? req.body.user.favoriteColor : null;

        // validate client's ssoToken
        const isAuthorized = await validateAccessToken(ssoToken);
        if (!isAuthorized) throw Error({ name: "Sample-Auth", message: "can't validate access token", "status": 401 });

        // construct scope for API call - must match registered scopes
        const oboRequest = {
            oboAssertion: ssoToken,
            scopes: ['User.Read'],
        }

        // get token on behalf of user
        let response = await cca.acquireTokenOnBehalfOf(oboRequest);
        if (!response.accessToken) throw Error({ name: "Sample-Auth", message: "no access token acquired", "status": 401 });

        // call API on behalf of user
        let apiResponse = await callResourceAPI(response.accessToken, 'https://graph.microsoft.com/v1.0/me');
        if (!apiResponse) throw Error({ name: "Sample-Graph", message: "call to Graph failed", "status": 500 });

        // MongoDB (CosmosDB) connect
        const mongoDBConnected = await UserService.connect();
        if (!mongoDBConnected) throw Error({ name: "Sample-DBConnection", message: "couldn't connect to database", "status": 500 });

        let foundUser = await UserService.getUserByEmail(apiResponse.mail);

        let mongodbUser = {};
        let update = false;

        if (!foundUser) {
            // create user
            mongodbUser = {
                customAppId: aadAppUniqueUser.payload.sub,
                name: apiResponse.displayName || null,  //displayName from Graph is source of truth
                email: apiResponse.mail || null,        //email from Graph is the source of true
                favoriteColor: null
            };
            update = true;
        }
        else if (foundUser && favoriteColor){
            mongodbUser = {
                customAppId: aadAppUniqueUser.payload.sub,
                name: apiResponse.displayName || null,  //displayName from Graph is source of truth
                email: apiResponse.mail || null,        //email from Graph is the source of true
                favoriteColor: favoriteColor
            };
            update=true;
        } else {
            // don't update because user not passed into API
            console.log("nothing to update");
        }

        // Upsert to MongoDB (CosmosDB)
        if(update){
            foundUser = await UserService.upsertByEmail(apiResponse.mail, mongodbUser);
            if (!foundUser) throw Error({ name: "Sample-DBConnection", message: "no user returned from database", "status": 500 });
        }

        // Return to client
        return context.res = {
            status: 200,
            body: {
                response: foundUser.toJSON() || null,
            },
            headers: {
                'Content-Type': 'application/json'
            }
        };

    } catch (error) {
        context.log(error);

        context.res = {
            status: error.status || 500,
            body: {
                response: error.message || JSON.stringify(error),
            }
        };
    }
}

/**
 * Makes an authorization bearer token request 
 * to given resource endpoint.
 */
callResourceAPI = async (newTokenValue, resourceURI) => {
    let options = {
        method: 'GET',
        headers: {
            'Authorization': `Bearer ${newTokenValue}`,
            'Content-type': 'application/json',
        },
    };

    let response = await fetch(resourceURI, options);
    let json = await response.json();
    return json;
}

/**
 * Validates the access token for signature 
 * and against a predefined set of claims
 */
validateAccessToken = async (accessToken) => {
    if (!accessToken || accessToken === "" || accessToken === "undefined") {
        console.log('No tokens found');
        return false;
    }

    // we will first decode to get kid parameter in header
    let decodedToken;

    try {
        decodedToken = jwt.decode(accessToken, { complete: true });
    } catch (error) {
        console.log('Token cannot be decoded');
        console.log(error);
        return false;
    }

    // obtains signing keys from discovery endpoint
    let keys;

    try {
        keys = await getSigningKeys(decodedToken.header);
    } catch (error) {
        console.log('Signing keys cannot be obtained');
        console.log(error);
        return false;
    }

    // verify the signature at header section using keys
    let verifiedToken;

    try {
        verifiedToken = jwt.verify(accessToken, keys);
    } catch (error) {
        console.log('Token cannot be verified');
        console.log(error);
        return false;
    }

    /**
     * Validates the token against issuer, audience, scope
     * and timestamp, though implementation and extent vary. For more information, visit:
     * https://learn.microsoft.com/azure/active-directory/develop/access-tokens#validating-tokens
     */

    const now = Math.round((new Date()).getTime() / 1000); // in UNIX format

    const checkTimestamp = verifiedToken["iat"] <= now && verifiedToken["exp"] >= now ? true : false;
    const checkAudience = verifiedToken['aud'] === process.env['CLIENT_ID'] || verifiedToken['aud'] === 'api://' + process.env['CLIENT_ID'] ? true : false;
    const checkScope = verifiedToken['scp'] === process.env['EXPECTED_SCOPES'] ? true : false;
    const checkIssuer = verifiedToken['iss'].includes(process.env['TENANT_INFO']) ? true : false;

    if (checkTimestamp && checkAudience && checkScope && checkIssuer) {
        // capture decodedToken, because sub is user's unique ID
        // for the Active Directory app
        aadAppUniqueUser=decodedToken;
        return true;
    }
    return false;
}

/**
 * Fetches signing keys of an access token 
 * from the authority discovery endpoint
 */
getSigningKeys = async (header) => {
    // In single-tenant apps, discovery keys endpoint will be specific to your tenant
    const jwksUri = `https://login.microsoftonline.com/${process.env['TENANT_INFO']}/discovery/v2.0/keys`
    console.log(jwksUri);

    const client = jwksClient({
        jwksUri: jwksUri
    });

    return (await client.getSigningKeyAsync(header.kid)).getPublicKey();
};

API Function: Perbarui local.settings.json dengan string koneksi MongoDB Anda

  1. Di Visual Studio Code, pilih penjelajah Azure, kemudian klik kanan pada sumber daya Cosmos DB Anda dan pilih Salin String Koneksi.

  2. Buka file, ./api/local.settings.json, dan tambahkan properti baru ke objek Values dan tempel di string koneksi Anda. Properti berikut adalah contoh pasangan nama/nilai:

    "MONGODB_URL":"mongodb://YOUR-RESOURCE-NAME:..."
    

Jalankan klien React dan API Function secara lokal

  1. Buka terminal terintegrasi dan jalankan aplikasi React dengan perintah berikut:

    npm start
    

    Aplikasi ini dimulai pada port 3000 dan harus dibuka di browser saat aplikasi sudah siap.

  2. Di Visual Studio Code, klik kanan direktori api dan pilih Buka di Terminal Terintegrasi dan jalankan perintah berikut:

    npm start
    

    Aplikasi ini dimulai di port 7071.

  3. Gunakan aplikasi untuk masuk ke aplikasi tersebut, dan pilih API Function dari menu atas.

    Halaman tersambung ke API Function, mendapatkan informasi Graph pengguna yang diautentikasi, kemudian memasukkan dokumen pengguna ke MongoDB termasuk data berikut:

    customAppId:    // user's unique ID for your Active Directory app
    name:           // user's name from Graph
    email:          // user's mail from Graph
    favoriteColor:  // user's favoriteColor send from React client form
    

    favoriteColor akan bernilai null ketika Anda pertama kali berkunjung ke halaman tersebut.

  4. Masukkan warna favorit Anda, seperti blue atau green dan pilih tombol Enter.

    Halaman ini tersambung ke API Function, dan menambahkan favoriteColor pengguna ke dokumen pengguna.

Lihat data Cosmos DB di portal Microsoft Azure

  1. Di Visual Studio Code, pilih penjelajah Azure, kemudian klik kanan sumber daya Cosmos DB, dan pilih Buka di Portal.

  2. Buka Data Explorer pilih katalog pengujian, kemudian pilih pengguna.

  3. Pilih id untuk melihat tampilan dokumen pengguna dalam daftar.

    Cara menggunakan mongodb unique values

Pertanyaan dan masalah desain

PertanyaanJawaban
Mengapa Anda tidak membuat jaringan virtual untuk mengamankan database? Desain membuat keputusan untuk mengamankan database dengan usaha minimal untuk seri artikel singkat ini. Jika Anda berencana untuk menyimpan sumber daya ini untuk durasi yang lebih lama, pindah ke jaringan virtual adalah pilihan keamanan yang disarankan.
Mengapa katalog ini diberi nama tes? String koneksi Cosmos DB tidak memberi nama katalog, sehingga menggunakan nama katalog default. Jika Anda lebih suka memberi nama pada katalog yang berbeda, tambahkan nama tersebut ke string koneksi setelah nomor port. Misalnya, jika Anda ingin nama katalog menjadi msal-sample, bagian string koneksi mungkin terlihat seperti mongodb://YOUR-RESOURCE-NAME:...:10255/msal-sample?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=YOUR-RESOURCE-NAME

Pemecahan Masalah

PertanyaanJawaban
Saya tidak dapat tersambung ke database Cosmos DB melalui kode JavaScript yang berjalan secara lokal di workstation saya. Verifikasi IP lokal Anda telah ditambahkan ke firewall database.

Langkah berikutnya

  • Menyebarkan aplikasi web Statik ke Azure